@amg555/frameforge-mcp 2.7.6 → 2.7.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +8 -0
- package/package.json +1 -1
- package/plugins/video-pipeline/.claude-plugin/plugin.json +1 -1
- package/plugins/video-pipeline/skills/.upstream-skills.json +1 -1
- package/plugins/video-pipeline/skills/video-pipeline/references/mcp-tools.md +1 -1
- package/scripts/deep-validate.js +2 -2
- package/src/mcp-server/server.js +5 -5
- package/src/skills/sync-upstream.js +3 -3
- package/.agent/skills/ai-video-editor/README.md +0 -32
- package/.agent/skills/ai-video-editor/SKILL.md +0 -152
- package/.agent/skills/ai-video-editor/assets/fonts/fonts.css +0 -2409
- package/.agent/skills/ai-video-editor/assets/fonts/ttf/anton-400.ttf +0 -0
- package/.agent/skills/ai-video-editor/assets/fonts/ttf/archivo-800.ttf +0 -0
- package/.agent/skills/ai-video-editor/assets/fonts/ttf/cabin-sketch-700.ttf +0 -0
- package/.agent/skills/ai-video-editor/assets/fonts/ttf/dm-sans-700.ttf +0 -0
- package/.agent/skills/ai-video-editor/assets/fonts/ttf/fraunces-600.ttf +0 -0
- package/.agent/skills/ai-video-editor/assets/fonts/ttf/fraunces-700.ttf +0 -0
- package/.agent/skills/ai-video-editor/assets/fonts/ttf/fredoka-700.ttf +0 -0
- package/.agent/skills/ai-video-editor/assets/fonts/ttf/inter-700.ttf +0 -0
- package/.agent/skills/ai-video-editor/assets/fonts/ttf/inter-800.ttf +0 -0
- package/.agent/skills/ai-video-editor/assets/fonts/ttf/inter-tight-800.ttf +0 -0
- package/.agent/skills/ai-video-editor/assets/fonts/ttf/kalam-700.ttf +0 -0
- package/.agent/skills/ai-video-editor/assets/fonts/ttf/noto-sans-devanagari-700.ttf +0 -0
- package/.agent/skills/ai-video-editor/assets/fonts/web/noto-sans-devanagari-devanagari-400-normal.woff2 +0 -0
- package/.agent/skills/ai-video-editor/assets/fonts/web/noto-sans-devanagari-devanagari-500-normal.woff2 +0 -0
- package/.agent/skills/ai-video-editor/assets/fonts/web/noto-sans-devanagari-devanagari-600-normal.woff2 +0 -0
- package/.agent/skills/ai-video-editor/assets/fonts/web/noto-sans-devanagari-devanagari-700-normal.woff2 +0 -0
- package/.agent/skills/ai-video-editor/assets/fonts/web/noto-sans-devanagari-devanagari-800-normal.woff2 +0 -0
- package/.agent/skills/ai-video-editor/engine/hidpi.js +0 -117
- package/.agent/skills/ai-video-editor/engine/page.html +0 -43
- package/.agent/skills/ai-video-editor/engine/player.js +0 -145
- package/.agent/skills/ai-video-editor/references/example-story-warm-canvas.js +0 -95
- package/.agent/skills/ai-video-editor/references/story-guide.md +0 -112
- package/.agent/skills/ai-video-editor/requirements.txt +0 -4
- package/.agent/skills/ai-video-editor/styles/chalk-talk/base.js +0 -490
- package/.agent/skills/ai-video-editor/styles/chalk-talk/style.json +0 -14
- package/.agent/skills/ai-video-editor/styles/chalk-talk/style.md +0 -41
- package/.agent/skills/ai-video-editor/styles/clean-tech/base.js +0 -447
- package/.agent/skills/ai-video-editor/styles/clean-tech/style.json +0 -15
- package/.agent/skills/ai-video-editor/styles/clean-tech/style.md +0 -38
- package/.agent/skills/ai-video-editor/styles/creator-motion-graphics/base.js +0 -698
- package/.agent/skills/ai-video-editor/styles/creator-motion-graphics/style.json +0 -16
- package/.agent/skills/ai-video-editor/styles/creator-motion-graphics/style.md +0 -39
- package/.agent/skills/ai-video-editor/styles/data-story/base.js +0 -432
- package/.agent/skills/ai-video-editor/styles/data-story/style.json +0 -14
- package/.agent/skills/ai-video-editor/styles/data-story/style.md +0 -38
- package/.agent/skills/ai-video-editor/styles/index.json +0 -103
- package/.agent/skills/ai-video-editor/styles/pop-info/base.js +0 -570
- package/.agent/skills/ai-video-editor/styles/pop-info/style.json +0 -13
- package/.agent/skills/ai-video-editor/styles/pop-info/style.md +0 -37
- package/.agent/skills/ai-video-editor/styles/software-walkthrough/base.js +0 -544
- package/.agent/skills/ai-video-editor/styles/software-walkthrough/style.json +0 -15
- package/.agent/skills/ai-video-editor/styles/software-walkthrough/style.md +0 -38
- package/.agent/skills/ai-video-editor/styles/warm-canvas/base.js +0 -610
- package/.agent/skills/ai-video-editor/styles/warm-canvas/style.json +0 -14
- package/.agent/skills/ai-video-editor/styles/warm-canvas/style.md +0 -37
- package/.agent/skills/faceless-shorts/README.md +0 -34
- package/.agent/skills/faceless-shorts/SKILL.md +0 -124
- package/.agent/skills/faceless-shorts/styles/deep-dive/STYLE.md +0 -58
- package/.agent/skills/faceless-shorts/styles/deep-dive/example/data/cues.json +0 -140
- package/.agent/skills/faceless-shorts/styles/deep-dive/example/data/lines.json +0 -67
- package/.agent/skills/faceless-shorts/styles/deep-dive/example/data/words.json +0 -614
- package/.agent/skills/faceless-shorts/styles/deep-dive/example/data/words.txt +0 -102
- package/.agent/skills/faceless-shorts/styles/deep-dive/example/fonts/Inter-Black.otf +0 -0
- package/.agent/skills/faceless-shorts/styles/deep-dive/example/fonts/Inter-ExtraBold.otf +0 -0
- package/.agent/skills/faceless-shorts/styles/deep-dive/example/index.html +0 -16
- package/.agent/skills/faceless-shorts/styles/deep-dive/example/script.json +0 -27
- package/.agent/skills/faceless-shorts/styles/deep-dive/example/sheet.html +0 -10
- package/.agent/skills/faceless-shorts/styles/deep-dive/example/src/core/board.js +0 -62
- package/.agent/skills/faceless-shorts/styles/deep-dive/example/src/core/captions.js +0 -68
- package/.agent/skills/faceless-shorts/styles/deep-dive/example/src/core/engine.js +0 -65
- package/.agent/skills/faceless-shorts/styles/deep-dive/example/src/core/human.js +0 -128
- package/.agent/skills/faceless-shorts/styles/deep-dive/example/src/core/lib.js +0 -44
- package/.agent/skills/faceless-shorts/styles/deep-dive/example/src/core/look2d.js +0 -33
- package/.agent/skills/faceless-shorts/styles/deep-dive/example/src/core/sdf.js +0 -79
- package/.agent/skills/faceless-shorts/styles/deep-dive/example/src/core/sets.js +0 -107
- package/.agent/skills/faceless-shorts/styles/deep-dive/example/src/core/timing.js +0 -23
- package/.agent/skills/faceless-shorts/styles/deep-dive/example/src/main.js +0 -56
- package/.agent/skills/faceless-shorts/styles/deep-dive/example/src/sheet.js +0 -79
- package/.agent/skills/faceless-shorts/styles/deep-dive/example/src/shots.js +0 -136
- package/.agent/skills/faceless-shorts/styles/deep-dive/example/tools/capture.mjs +0 -122
- package/.agent/skills/faceless-shorts/styles/deep-dive/example/tools/mix.py +0 -172
- package/.agent/skills/faceless-shorts/styles/deep-dive/example/vendor/LICENSE-three.txt +0 -21
- package/.agent/skills/faceless-shorts/styles/deep-dive/example/vendor/addons/environments/RoomEnvironment.js +0 -144
- package/.agent/skills/faceless-shorts/styles/deep-dive/example/vendor/addons/postprocessing/BokehPass.js +0 -141
- package/.agent/skills/faceless-shorts/styles/deep-dive/example/vendor/addons/postprocessing/EffectComposer.js +0 -231
- package/.agent/skills/faceless-shorts/styles/deep-dive/example/vendor/addons/postprocessing/MaskPass.js +0 -104
- package/.agent/skills/faceless-shorts/styles/deep-dive/example/vendor/addons/postprocessing/OutputPass.js +0 -97
- package/.agent/skills/faceless-shorts/styles/deep-dive/example/vendor/addons/postprocessing/Pass.js +0 -95
- package/.agent/skills/faceless-shorts/styles/deep-dive/example/vendor/addons/postprocessing/RenderPass.js +0 -99
- package/.agent/skills/faceless-shorts/styles/deep-dive/example/vendor/addons/postprocessing/ShaderPass.js +0 -77
- package/.agent/skills/faceless-shorts/styles/deep-dive/example/vendor/addons/postprocessing/UnrealBloomPass.js +0 -415
- package/.agent/skills/faceless-shorts/styles/deep-dive/example/vendor/addons/shaders/BokehShader.js +0 -145
- package/.agent/skills/faceless-shorts/styles/deep-dive/example/vendor/addons/shaders/CopyShader.js +0 -45
- package/.agent/skills/faceless-shorts/styles/deep-dive/example/vendor/addons/shaders/LuminosityHighPassShader.js +0 -64
- package/.agent/skills/faceless-shorts/styles/deep-dive/example/vendor/addons/shaders/OutputShader.js +0 -85
- package/.agent/skills/faceless-shorts/styles/deep-dive/example/vendor/three.module.js +0 -6
- package/.agent/skills/faceless-shorts/styles/deep-dive/style-sheet.png +0 -0
- package/.agent/skills/faceless-shorts/styles/inside-out/STYLE.md +0 -97
- package/.agent/skills/faceless-shorts/styles/inside-out/example/data/cues.json +0 -178
- package/.agent/skills/faceless-shorts/styles/inside-out/example/data/lines.json +0 -73
- package/.agent/skills/faceless-shorts/styles/inside-out/example/data/words.json +0 -662
- package/.agent/skills/faceless-shorts/styles/inside-out/example/fonts/Inter-Black.otf +0 -0
- package/.agent/skills/faceless-shorts/styles/inside-out/example/index.html +0 -16
- package/.agent/skills/faceless-shorts/styles/inside-out/example/script.json +0 -26
- package/.agent/skills/faceless-shorts/styles/inside-out/example/src/core/captions.js +0 -68
- package/.agent/skills/faceless-shorts/styles/inside-out/example/src/core/kit.js +0 -345
- package/.agent/skills/faceless-shorts/styles/inside-out/example/src/core/lib.js +0 -44
- package/.agent/skills/faceless-shorts/styles/inside-out/example/src/core/timing.js +0 -23
- package/.agent/skills/faceless-shorts/styles/inside-out/example/src/main.js +0 -62
- package/.agent/skills/faceless-shorts/styles/inside-out/example/src/shots.js +0 -195
- package/.agent/skills/faceless-shorts/styles/inside-out/example/tools/capture.mjs +0 -122
- package/.agent/skills/faceless-shorts/styles/inside-out/example/tools/mix.py +0 -172
- package/.agent/skills/faceless-shorts/styles/inside-out/style-sheet.png +0 -0
- package/.agent/skills/faceless-shorts/styles/night-file/STYLE.md +0 -36
- package/.agent/skills/faceless-shorts/styles/night-file/example/data/cues.json +0 -146
- package/.agent/skills/faceless-shorts/styles/night-file/example/data/lines.json +0 -73
- package/.agent/skills/faceless-shorts/styles/night-file/example/data/words.json +0 -572
- package/.agent/skills/faceless-shorts/styles/night-file/example/data/words.txt +0 -95
- package/.agent/skills/faceless-shorts/styles/night-file/example/fonts/FreeMonoBold.ttf +0 -0
- package/.agent/skills/faceless-shorts/styles/night-file/example/fonts/Inter-Black.otf +0 -0
- package/.agent/skills/faceless-shorts/styles/night-file/example/index.html +0 -16
- package/.agent/skills/faceless-shorts/styles/night-file/example/script.json +0 -26
- package/.agent/skills/faceless-shorts/styles/night-file/example/sheet.html +0 -11
- package/.agent/skills/faceless-shorts/styles/night-file/example/src/core/board.js +0 -62
- package/.agent/skills/faceless-shorts/styles/night-file/example/src/core/captions.js +0 -49
- package/.agent/skills/faceless-shorts/styles/night-file/example/src/core/engine.js +0 -65
- package/.agent/skills/faceless-shorts/styles/night-file/example/src/core/human.js +0 -128
- package/.agent/skills/faceless-shorts/styles/night-file/example/src/core/kit.js +0 -151
- package/.agent/skills/faceless-shorts/styles/night-file/example/src/core/lib.js +0 -44
- package/.agent/skills/faceless-shorts/styles/night-file/example/src/core/sdf.js +0 -79
- package/.agent/skills/faceless-shorts/styles/night-file/example/src/core/sets3d.js +0 -106
- package/.agent/skills/faceless-shorts/styles/night-file/example/src/core/timing.js +0 -23
- package/.agent/skills/faceless-shorts/styles/night-file/example/src/main.js +0 -56
- package/.agent/skills/faceless-shorts/styles/night-file/example/src/sheet.js +0 -86
- package/.agent/skills/faceless-shorts/styles/night-file/example/src/shots.js +0 -130
- package/.agent/skills/faceless-shorts/styles/night-file/example/tools/capture.mjs +0 -122
- package/.agent/skills/faceless-shorts/styles/night-file/example/tools/mix.py +0 -172
- package/.agent/skills/faceless-shorts/styles/night-file/example/vendor/LICENSE-three.txt +0 -21
- package/.agent/skills/faceless-shorts/styles/night-file/example/vendor/addons/environments/RoomEnvironment.js +0 -144
- package/.agent/skills/faceless-shorts/styles/night-file/example/vendor/addons/postprocessing/BokehPass.js +0 -141
- package/.agent/skills/faceless-shorts/styles/night-file/example/vendor/addons/postprocessing/EffectComposer.js +0 -231
- package/.agent/skills/faceless-shorts/styles/night-file/example/vendor/addons/postprocessing/MaskPass.js +0 -104
- package/.agent/skills/faceless-shorts/styles/night-file/example/vendor/addons/postprocessing/OutputPass.js +0 -97
- package/.agent/skills/faceless-shorts/styles/night-file/example/vendor/addons/postprocessing/Pass.js +0 -95
- package/.agent/skills/faceless-shorts/styles/night-file/example/vendor/addons/postprocessing/RenderPass.js +0 -99
- package/.agent/skills/faceless-shorts/styles/night-file/example/vendor/addons/postprocessing/ShaderPass.js +0 -77
- package/.agent/skills/faceless-shorts/styles/night-file/example/vendor/addons/postprocessing/UnrealBloomPass.js +0 -415
- package/.agent/skills/faceless-shorts/styles/night-file/example/vendor/addons/shaders/BokehShader.js +0 -145
- package/.agent/skills/faceless-shorts/styles/night-file/example/vendor/addons/shaders/CopyShader.js +0 -45
- package/.agent/skills/faceless-shorts/styles/night-file/example/vendor/addons/shaders/LuminosityHighPassShader.js +0 -64
- package/.agent/skills/faceless-shorts/styles/night-file/example/vendor/addons/shaders/OutputShader.js +0 -85
- package/.agent/skills/faceless-shorts/styles/night-file/example/vendor/three.module.js +0 -6
- package/.agent/skills/faceless-shorts/styles/night-file/style-sheet.png +0 -0
- package/.agent/skills/faceless-shorts/styles/paper-atlas/STYLE.md +0 -30
- package/.agent/skills/faceless-shorts/styles/paper-atlas/example/data/cues.json +0 -167
- package/.agent/skills/faceless-shorts/styles/paper-atlas/example/data/lines.json +0 -67
- package/.agent/skills/faceless-shorts/styles/paper-atlas/example/data/words.json +0 -614
- package/.agent/skills/faceless-shorts/styles/paper-atlas/example/data/words.txt +0 -102
- package/.agent/skills/faceless-shorts/styles/paper-atlas/example/fonts/Caladea-Bold.ttf +0 -0
- package/.agent/skills/faceless-shorts/styles/paper-atlas/example/fonts/Inter-Black.otf +0 -0
- package/.agent/skills/faceless-shorts/styles/paper-atlas/example/index.html +0 -13
- package/.agent/skills/faceless-shorts/styles/paper-atlas/example/script.json +0 -26
- package/.agent/skills/faceless-shorts/styles/paper-atlas/example/sheet.html +0 -10
- package/.agent/skills/faceless-shorts/styles/paper-atlas/example/src/core/board.js +0 -62
- package/.agent/skills/faceless-shorts/styles/paper-atlas/example/src/core/captions.js +0 -47
- package/.agent/skills/faceless-shorts/styles/paper-atlas/example/src/core/kit.js +0 -152
- package/.agent/skills/faceless-shorts/styles/paper-atlas/example/src/core/lib.js +0 -44
- package/.agent/skills/faceless-shorts/styles/paper-atlas/example/src/core/timing.js +0 -23
- package/.agent/skills/faceless-shorts/styles/paper-atlas/example/src/main.js +0 -62
- package/.agent/skills/faceless-shorts/styles/paper-atlas/example/src/sheet.js +0 -61
- package/.agent/skills/faceless-shorts/styles/paper-atlas/example/src/shots.js +0 -139
- package/.agent/skills/faceless-shorts/styles/paper-atlas/example/tools/capture.mjs +0 -122
- package/.agent/skills/faceless-shorts/styles/paper-atlas/example/tools/mix.py +0 -172
- package/.agent/skills/faceless-shorts/styles/paper-atlas/style-sheet.png +0 -0
- package/.agent/skills/faceless-shorts/styles/stick-story/STYLE.md +0 -32
- package/.agent/skills/faceless-shorts/styles/stick-story/example/data/cues.json +0 -182
- package/.agent/skills/faceless-shorts/styles/stick-story/example/data/lines.json +0 -79
- package/.agent/skills/faceless-shorts/styles/stick-story/example/data/words.json +0 -662
- package/.agent/skills/faceless-shorts/styles/stick-story/example/data/words.txt +0 -110
- package/.agent/skills/faceless-shorts/styles/stick-story/example/fonts/Inter-Black.otf +0 -0
- package/.agent/skills/faceless-shorts/styles/stick-story/example/fonts/Inter-ExtraBold.otf +0 -0
- package/.agent/skills/faceless-shorts/styles/stick-story/example/index.html +0 -12
- package/.agent/skills/faceless-shorts/styles/stick-story/example/script.json +0 -27
- package/.agent/skills/faceless-shorts/styles/stick-story/example/sheet.html +0 -9
- package/.agent/skills/faceless-shorts/styles/stick-story/example/src/core/board.js +0 -62
- package/.agent/skills/faceless-shorts/styles/stick-story/example/src/core/captions.js +0 -47
- package/.agent/skills/faceless-shorts/styles/stick-story/example/src/core/kit.js +0 -166
- package/.agent/skills/faceless-shorts/styles/stick-story/example/src/core/lib.js +0 -44
- package/.agent/skills/faceless-shorts/styles/stick-story/example/src/core/timing.js +0 -23
- package/.agent/skills/faceless-shorts/styles/stick-story/example/src/main.js +0 -62
- package/.agent/skills/faceless-shorts/styles/stick-story/example/src/sheet.js +0 -64
- package/.agent/skills/faceless-shorts/styles/stick-story/example/src/shots.js +0 -148
- package/.agent/skills/faceless-shorts/styles/stick-story/example/tools/capture.mjs +0 -122
- package/.agent/skills/faceless-shorts/styles/stick-story/example/tools/mix.py +0 -172
- package/.agent/skills/faceless-shorts/styles/stick-story/style-sheet.png +0 -0
- package/.agent/skills/faceless-shorts/styles/tall-tale/STYLE.md +0 -51
- package/.agent/skills/faceless-shorts/styles/tall-tale/example/data/cues.json +0 -271
- package/.agent/skills/faceless-shorts/styles/tall-tale/example/data/lines.json +0 -127
- package/.agent/skills/faceless-shorts/styles/tall-tale/example/data/words.json +0 -908
- package/.agent/skills/faceless-shorts/styles/tall-tale/example/data/words.txt +0 -151
- package/.agent/skills/faceless-shorts/styles/tall-tale/example/fonts/Caladea-Bold.ttf +0 -0
- package/.agent/skills/faceless-shorts/styles/tall-tale/example/fonts/Inter-Black.otf +0 -0
- package/.agent/skills/faceless-shorts/styles/tall-tale/example/fonts/Inter-ExtraBold.otf +0 -0
- package/.agent/skills/faceless-shorts/styles/tall-tale/example/index.html +0 -17
- package/.agent/skills/faceless-shorts/styles/tall-tale/example/script.json +0 -37
- package/.agent/skills/faceless-shorts/styles/tall-tale/example/sheet.html +0 -11
- package/.agent/skills/faceless-shorts/styles/tall-tale/example/src/core/board.js +0 -62
- package/.agent/skills/faceless-shorts/styles/tall-tale/example/src/core/captions.js +0 -56
- package/.agent/skills/faceless-shorts/styles/tall-tale/example/src/core/engine.js +0 -65
- package/.agent/skills/faceless-shorts/styles/tall-tale/example/src/core/lib.js +0 -44
- package/.agent/skills/faceless-shorts/styles/tall-tale/example/src/core/props.js +0 -166
- package/.agent/skills/faceless-shorts/styles/tall-tale/example/src/core/timing.js +0 -23
- package/.agent/skills/faceless-shorts/styles/tall-tale/example/src/core/toon.js +0 -352
- package/.agent/skills/faceless-shorts/styles/tall-tale/example/src/core/world.js +0 -340
- package/.agent/skills/faceless-shorts/styles/tall-tale/example/src/main.js +0 -56
- package/.agent/skills/faceless-shorts/styles/tall-tale/example/src/sheet.js +0 -99
- package/.agent/skills/faceless-shorts/styles/tall-tale/example/src/shots.js +0 -377
- package/.agent/skills/faceless-shorts/styles/tall-tale/example/tools/capture.mjs +0 -122
- package/.agent/skills/faceless-shorts/styles/tall-tale/example/tools/mix.py +0 -204
- package/.agent/skills/faceless-shorts/styles/tall-tale/example/vendor/LICENSE-three.txt +0 -21
- package/.agent/skills/faceless-shorts/styles/tall-tale/example/vendor/addons/environments/RoomEnvironment.js +0 -144
- package/.agent/skills/faceless-shorts/styles/tall-tale/example/vendor/addons/postprocessing/BokehPass.js +0 -141
- package/.agent/skills/faceless-shorts/styles/tall-tale/example/vendor/addons/postprocessing/EffectComposer.js +0 -231
- package/.agent/skills/faceless-shorts/styles/tall-tale/example/vendor/addons/postprocessing/MaskPass.js +0 -104
- package/.agent/skills/faceless-shorts/styles/tall-tale/example/vendor/addons/postprocessing/OutputPass.js +0 -97
- package/.agent/skills/faceless-shorts/styles/tall-tale/example/vendor/addons/postprocessing/Pass.js +0 -95
- package/.agent/skills/faceless-shorts/styles/tall-tale/example/vendor/addons/postprocessing/RenderPass.js +0 -99
- package/.agent/skills/faceless-shorts/styles/tall-tale/example/vendor/addons/postprocessing/ShaderPass.js +0 -77
- package/.agent/skills/faceless-shorts/styles/tall-tale/example/vendor/addons/postprocessing/UnrealBloomPass.js +0 -415
- package/.agent/skills/faceless-shorts/styles/tall-tale/example/vendor/addons/shaders/BokehShader.js +0 -145
- package/.agent/skills/faceless-shorts/styles/tall-tale/example/vendor/addons/shaders/CopyShader.js +0 -45
- package/.agent/skills/faceless-shorts/styles/tall-tale/example/vendor/addons/shaders/LuminosityHighPassShader.js +0 -64
- package/.agent/skills/faceless-shorts/styles/tall-tale/example/vendor/addons/shaders/OutputShader.js +0 -85
- package/.agent/skills/faceless-shorts/styles/tall-tale/example/vendor/three.module.js +0 -6
- package/.agent/skills/faceless-shorts/styles/tall-tale/style-sheet.png +0 -0
- package/.agent/skills/gstack-autoplan/SKILL.md +0 -972
- package/.agent/skills/gstack-autoplan/SKILL.md.tmpl +0 -468
- package/.agent/skills/gstack-autoplan/bin/guard-journal.ts +0 -115
- package/.agent/skills/gstack-autoplan/bin/guard-log.ts +0 -40
- package/.agent/skills/gstack-autoplan/bin/guard-reasons.ts +0 -92
- package/.agent/skills/gstack-autoplan/bin/owned-read.ts +0 -72
- package/.agent/skills/gstack-autoplan/bin/phase-publication-hook +0 -15
- package/.agent/skills/gstack-autoplan/bin/phase-publication-hook.ts +0 -769
- package/.agent/skills/gstack-autoplan/sections/ceo-phase.md +0 -195
- package/.agent/skills/gstack-autoplan/sections/ceo-phase.md.tmpl +0 -147
- package/.agent/skills/gstack-autoplan/sections/design-phase.md +0 -128
- package/.agent/skills/gstack-autoplan/sections/design-phase.md.tmpl +0 -82
- package/.agent/skills/gstack-autoplan/sections/dx-phase.md +0 -155
- package/.agent/skills/gstack-autoplan/sections/dx-phase.md.tmpl +0 -109
- package/.agent/skills/gstack-autoplan/sections/eng-phase.md +0 -164
- package/.agent/skills/gstack-autoplan/sections/eng-phase.md.tmpl +0 -116
- package/.agent/skills/gstack-autoplan/sections/manifest.json +0 -50
- package/.agent/skills/gstack-autoplan/sections/phase-close.md +0 -70
- package/.agent/skills/gstack-autoplan/sections/phase-close.md.tmpl +0 -68
- package/.agent/skills/gstack-autoplan/sections/tasks-aggregator.md +0 -71
- package/.agent/skills/gstack-autoplan/sections/tasks-aggregator.md.tmpl +0 -1
- package/.agent/skills/gstack-autoplan/sections/unattended.md +0 -11
- package/.agent/skills/gstack-autoplan/sections/unattended.md.tmpl +0 -9
- package/.agent/skills/gstack-benchmark/SKILL.md +0 -456
- package/.agent/skills/gstack-benchmark/SKILL.md.tmpl +0 -249
- package/.agent/skills/gstack-benchmark-models/SKILL.md +0 -285
- package/.agent/skills/gstack-benchmark-models/SKILL.md.tmpl +0 -155
- package/.agent/skills/gstack-browse/PLAN-snapshot-dropdown-interactive.md +0 -102
- package/.agent/skills/gstack-browse/SKILL.md +0 -470
- package/.agent/skills/gstack-browse/SKILL.md.tmpl +0 -129
- package/.agent/skills/gstack-browse/bin/find-browse +0 -21
- package/.agent/skills/gstack-browse/bin/remote-slug +0 -18
- package/.agent/skills/gstack-browse/sections/command-list.md +0 -176
- package/.agent/skills/gstack-browse/sections/command-list.md.tmpl +0 -7
- package/.agent/skills/gstack-browse/sections/manifest.json +0 -14
- package/.agent/skills/gstack-browse/src/activity.ts +0 -209
- package/.agent/skills/gstack-browse/src/audit.ts +0 -69
- package/.agent/skills/gstack-browse/src/browse-client.ts +0 -264
- package/.agent/skills/gstack-browse/src/browser-manager.ts +0 -2334
- package/.agent/skills/gstack-browse/src/browser-skill-commands.ts +0 -466
- package/.agent/skills/gstack-browse/src/browser-skill-write.ts +0 -216
- package/.agent/skills/gstack-browse/src/browser-skills.ts +0 -420
- package/.agent/skills/gstack-browse/src/buffers.ts +0 -137
- package/.agent/skills/gstack-browse/src/bun-child-config.ts +0 -35
- package/.agent/skills/gstack-browse/src/bun-polyfill.cjs +0 -276
- package/.agent/skills/gstack-browse/src/cdp-allowlist.ts +0 -235
- package/.agent/skills/gstack-browse/src/cdp-bridge.ts +0 -180
- package/.agent/skills/gstack-browse/src/cdp-commands.ts +0 -64
- package/.agent/skills/gstack-browse/src/cdp-inspector.ts +0 -824
- package/.agent/skills/gstack-browse/src/chromium-profiles.ts +0 -212
- package/.agent/skills/gstack-browse/src/claude-bin.ts +0 -2
- package/.agent/skills/gstack-browse/src/cli.ts +0 -2254
- package/.agent/skills/gstack-browse/src/commands.ts +0 -296
- package/.agent/skills/gstack-browse/src/config.ts +0 -307
- package/.agent/skills/gstack-browse/src/content-security.ts +0 -373
- package/.agent/skills/gstack-browse/src/cookie-auth-verification.ts +0 -225
- package/.agent/skills/gstack-browse/src/cookie-database.ts +0 -73
- package/.agent/skills/gstack-browse/src/cookie-import-browser.ts +0 -929
- package/.agent/skills/gstack-browse/src/cookie-import-native-integrity.ts +0 -95
- package/.agent/skills/gstack-browse/src/cookie-import-native-job.ts +0 -161
- package/.agent/skills/gstack-browse/src/cookie-import-native-qualification.json +0 -1
- package/.agent/skills/gstack-browse/src/cookie-import-native-worker.ts +0 -268
- package/.agent/skills/gstack-browse/src/cookie-import-native.ts +0 -166
- package/.agent/skills/gstack-browse/src/cookie-import-operation.ts +0 -232
- package/.agent/skills/gstack-browse/src/cookie-picker-routes.ts +0 -343
- package/.agent/skills/gstack-browse/src/cookie-picker-ui.ts +0 -717
- package/.agent/skills/gstack-browse/src/domain-skill-commands.ts +0 -301
- package/.agent/skills/gstack-browse/src/domain-skills.ts +0 -438
- package/.agent/skills/gstack-browse/src/error-handling.ts +0 -2
- package/.agent/skills/gstack-browse/src/extension-id.ts +0 -25
- package/.agent/skills/gstack-browse/src/file-permissions.ts +0 -370
- package/.agent/skills/gstack-browse/src/find-browse.ts +0 -118
- package/.agent/skills/gstack-browse/src/find-security-sidecar.ts +0 -78
- package/.agent/skills/gstack-browse/src/media-extract.ts +0 -177
- package/.agent/skills/gstack-browse/src/memory-command.ts +0 -115
- package/.agent/skills/gstack-browse/src/memory-snapshot.ts +0 -73
- package/.agent/skills/gstack-browse/src/meta-commands.ts +0 -1150
- package/.agent/skills/gstack-browse/src/network-capture.ts +0 -179
- package/.agent/skills/gstack-browse/src/path-security.ts +0 -132
- package/.agent/skills/gstack-browse/src/platform.ts +0 -38
- package/.agent/skills/gstack-browse/src/port-allocator.ts +0 -140
- package/.agent/skills/gstack-browse/src/project-slug.ts +0 -36
- package/.agent/skills/gstack-browse/src/proxy-config.ts +0 -283
- package/.agent/skills/gstack-browse/src/proxy-redact.ts +0 -46
- package/.agent/skills/gstack-browse/src/pty-session-cookie.ts +0 -64
- package/.agent/skills/gstack-browse/src/pty-session-lease.ts +0 -137
- package/.agent/skills/gstack-browse/src/read-commands.ts +0 -756
- package/.agent/skills/gstack-browse/src/routes/activity.ts +0 -63
- package/.agent/skills/gstack-browse/src/routes/commands.ts +0 -154
- package/.agent/skills/gstack-browse/src/routes/core.ts +0 -123
- package/.agent/skills/gstack-browse/src/routes/files.ts +0 -47
- package/.agent/skills/gstack-browse/src/routes/index.ts +0 -27
- package/.agent/skills/gstack-browse/src/routes/inspector.ts +0 -132
- package/.agent/skills/gstack-browse/src/routes/pairing.ts +0 -158
- package/.agent/skills/gstack-browse/src/routes/pty.ts +0 -254
- package/.agent/skills/gstack-browse/src/routes/table.ts +0 -182
- package/.agent/skills/gstack-browse/src/routes/tokens.ts +0 -86
- package/.agent/skills/gstack-browse/src/routes/tunnel.ts +0 -62
- package/.agent/skills/gstack-browse/src/sanitize.ts +0 -49
- package/.agent/skills/gstack-browse/src/screenshot-size-guard.ts +0 -106
- package/.agent/skills/gstack-browse/src/security-classifier.ts +0 -258
- package/.agent/skills/gstack-browse/src/security-sidecar-client.ts +0 -234
- package/.agent/skills/gstack-browse/src/security-sidecar-entry.ts +0 -120
- package/.agent/skills/gstack-browse/src/security.ts +0 -328
- package/.agent/skills/gstack-browse/src/server.ts +0 -2220
- package/.agent/skills/gstack-browse/src/session-cookie-store.ts +0 -124
- package/.agent/skills/gstack-browse/src/session-persist.ts +0 -178
- package/.agent/skills/gstack-browse/src/skill-token.ts +0 -91
- package/.agent/skills/gstack-browse/src/snapshot-flags.ts +0 -30
- package/.agent/skills/gstack-browse/src/snapshot.ts +0 -656
- package/.agent/skills/gstack-browse/src/socks-bridge.ts +0 -314
- package/.agent/skills/gstack-browse/src/sse-helpers.ts +0 -147
- package/.agent/skills/gstack-browse/src/sse-session-cookie.ts +0 -65
- package/.agent/skills/gstack-browse/src/stealth.ts +0 -574
- package/.agent/skills/gstack-browse/src/tab-session.ts +0 -210
- package/.agent/skills/gstack-browse/src/telemetry.ts +0 -115
- package/.agent/skills/gstack-browse/src/terminal-agent-control.ts +0 -379
- package/.agent/skills/gstack-browse/src/terminal-agent.ts +0 -1124
- package/.agent/skills/gstack-browse/src/terminal-pty-lifecycle.ts +0 -144
- package/.agent/skills/gstack-browse/src/token-registry.ts +0 -721
- package/.agent/skills/gstack-browse/src/tunnel-denial-log.ts +0 -97
- package/.agent/skills/gstack-browse/src/url-validation.ts +0 -299
- package/.agent/skills/gstack-browse/src/welcome.html +0 -237
- package/.agent/skills/gstack-browse/src/write-commands.ts +0 -1438
- package/.agent/skills/gstack-browse/src/xprotect-heal.ts +0 -439
- package/.agent/skills/gstack-browse/src/xvfb.ts +0 -313
- package/.agent/skills/gstack-browse/test/activity.test.ts +0 -120
- package/.agent/skills/gstack-browse/test/adversarial-security.test.ts +0 -32
- package/.agent/skills/gstack-browse/test/batch.test.ts +0 -267
- package/.agent/skills/gstack-browse/test/bridge-chromium-e2e.test.ts +0 -205
- package/.agent/skills/gstack-browse/test/browse-client.test.ts +0 -300
- package/.agent/skills/gstack-browse/test/browse-eval-wrapping.test.ts +0 -86
- package/.agent/skills/gstack-browse/test/browser-manager-custom-chromium.test.ts +0 -67
- package/.agent/skills/gstack-browse/test/browser-manager-unit.test.ts +0 -335
- package/.agent/skills/gstack-browse/test/browser-skill-commands.test.ts +0 -433
- package/.agent/skills/gstack-browse/test/browser-skill-write.test.ts +0 -350
- package/.agent/skills/gstack-browse/test/browser-skills-e2e.test.ts +0 -101
- package/.agent/skills/gstack-browse/test/browser-skills-storage.test.ts +0 -283
- package/.agent/skills/gstack-browse/test/build-command-response.test.ts +0 -68
- package/.agent/skills/gstack-browse/test/build.test.ts +0 -52
- package/.agent/skills/gstack-browse/test/bun-child-config.test.ts +0 -83
- package/.agent/skills/gstack-browse/test/bun-polyfill.test.ts +0 -402
- package/.agent/skills/gstack-browse/test/busy-daemon-iron-rule.test.ts +0 -270
- package/.agent/skills/gstack-browse/test/busy-daemon-recovery.test.ts +0 -134
- package/.agent/skills/gstack-browse/test/cdp-allowlist.test.ts +0 -109
- package/.agent/skills/gstack-browse/test/cdp-e2e.test.ts +0 -117
- package/.agent/skills/gstack-browse/test/cdp-inspector-history-cap.test.ts +0 -95
- package/.agent/skills/gstack-browse/test/cdp-mutex.test.ts +0 -113
- package/.agent/skills/gstack-browse/test/cdp-session-cleanup.test.ts +0 -171
- package/.agent/skills/gstack-browse/test/chromium-profile-isolation.test.ts +0 -418
- package/.agent/skills/gstack-browse/test/claude-bin.test.ts +0 -95
- package/.agent/skills/gstack-browse/test/cli-chain-stdin.test.ts +0 -122
- package/.agent/skills/gstack-browse/test/cli-config-mismatch-message.test.ts +0 -72
- package/.agent/skills/gstack-browse/test/cli-lock.test.ts +0 -120
- package/.agent/skills/gstack-browse/test/cli-loopback-no-proxy.test.ts +0 -77
- package/.agent/skills/gstack-browse/test/cli-setsid-daemonize.test.ts +0 -75
- package/.agent/skills/gstack-browse/test/cli-start-final-healthcheck.test.ts +0 -77
- package/.agent/skills/gstack-browse/test/cli-supervisor.test.ts +0 -194
- package/.agent/skills/gstack-browse/test/commands.test.ts +0 -2779
- package/.agent/skills/gstack-browse/test/compare-board.test.ts +0 -382
- package/.agent/skills/gstack-browse/test/config.test.ts +0 -519
- package/.agent/skills/gstack-browse/test/content-security.test.ts +0 -681
- package/.agent/skills/gstack-browse/test/cookie-auth-verification.test.ts +0 -760
- package/.agent/skills/gstack-browse/test/cookie-credential-deadline.test.ts +0 -225
- package/.agent/skills/gstack-browse/test/cookie-database.test.ts +0 -277
- package/.agent/skills/gstack-browse/test/cookie-fixture-delete-lease.test.ts +0 -256
- package/.agent/skills/gstack-browse/test/cookie-import-browser.test.ts +0 -523
- package/.agent/skills/gstack-browse/test/cookie-import-native-job.test.ts +0 -1645
- package/.agent/skills/gstack-browse/test/cookie-import-native-qualification.ts +0 -128
- package/.agent/skills/gstack-browse/test/cookie-import-native.test.ts +0 -436
- package/.agent/skills/gstack-browse/test/cookie-import-node.test.ts +0 -166
- package/.agent/skills/gstack-browse/test/cookie-import-operation.test.ts +0 -452
- package/.agent/skills/gstack-browse/test/cookie-import-reliability.test.ts +0 -659
- package/.agent/skills/gstack-browse/test/cookie-import-transport.test.ts +0 -38
- package/.agent/skills/gstack-browse/test/cookie-picker-binding-browser.test.ts +0 -96
- package/.agent/skills/gstack-browse/test/cookie-picker-binding.test.ts +0 -155
- package/.agent/skills/gstack-browse/test/cookie-picker-csrf-browser.test.ts +0 -50
- package/.agent/skills/gstack-browse/test/cookie-picker-routes.test.ts +0 -460
- package/.agent/skills/gstack-browse/test/cookie-picker-ui.test.ts +0 -535
- package/.agent/skills/gstack-browse/test/daemon-log-hygiene.test.ts +0 -135
- package/.agent/skills/gstack-browse/test/daemon-mismatch-refuse.test.ts +0 -178
- package/.agent/skills/gstack-browse/test/data-platform.test.ts +0 -176
- package/.agent/skills/gstack-browse/test/dia-gui-readiness.test.ts +0 -174
- package/.agent/skills/gstack-browse/test/dia-launch-comparison.test.ts +0 -180
- package/.agent/skills/gstack-browse/test/dia-macos-qualification.test.ts +0 -1695
- package/.agent/skills/gstack-browse/test/domain-skills-e2e.test.ts +0 -142
- package/.agent/skills/gstack-browse/test/domain-skills-storage.test.ts +0 -263
- package/.agent/skills/gstack-browse/test/dotenv-isolation.test.ts +0 -121
- package/.agent/skills/gstack-browse/test/dual-listener.test.ts +0 -411
- package/.agent/skills/gstack-browse/test/dx-polish.test.ts +0 -101
- package/.agent/skills/gstack-browse/test/error-handling.test.ts +0 -47
- package/.agent/skills/gstack-browse/test/extension-sender-auth.test.ts +0 -285
- package/.agent/skills/gstack-browse/test/extension-token.test.ts +0 -232
- package/.agent/skills/gstack-browse/test/file-drop.test.ts +0 -271
- package/.agent/skills/gstack-browse/test/file-permissions.test.ts +0 -346
- package/.agent/skills/gstack-browse/test/fill-change-event.test.ts +0 -78
- package/.agent/skills/gstack-browse/test/find-browse.test.ts +0 -61
- package/.agent/skills/gstack-browse/test/findport.test.ts +0 -232
- package/.agent/skills/gstack-browse/test/fixtures/basic.html +0 -33
- package/.agent/skills/gstack-browse/test/fixtures/change-only-validator.html +0 -31
- package/.agent/skills/gstack-browse/test/fixtures/cursor-interactive.html +0 -22
- package/.agent/skills/gstack-browse/test/fixtures/dialog.html +0 -15
- package/.agent/skills/gstack-browse/test/fixtures/dropdown.html +0 -61
- package/.agent/skills/gstack-browse/test/fixtures/empty.html +0 -2
- package/.agent/skills/gstack-browse/test/fixtures/forms.html +0 -55
- package/.agent/skills/gstack-browse/test/fixtures/iframe.html +0 -30
- package/.agent/skills/gstack-browse/test/fixtures/injection-combined.html +0 -58
- package/.agent/skills/gstack-browse/test/fixtures/injection-hidden.html +0 -62
- package/.agent/skills/gstack-browse/test/fixtures/injection-social.html +0 -25
- package/.agent/skills/gstack-browse/test/fixtures/injection-visible.html +0 -26
- package/.agent/skills/gstack-browse/test/fixtures/media-page.html +0 -67
- package/.agent/skills/gstack-browse/test/fixtures/mock-claude/claude +0 -185
- package/.agent/skills/gstack-browse/test/fixtures/native-cookie-delete-lease.ts +0 -125
- package/.agent/skills/gstack-browse/test/fixtures/native-cookie-file-owners.ts +0 -132
- package/.agent/skills/gstack-browse/test/fixtures/native-cookie-launch.cjs +0 -139
- package/.agent/skills/gstack-browse/test/fixtures/native-cookie-process-observer.ts +0 -150
- package/.agent/skills/gstack-browse/test/fixtures/native-cookie-process.cjs +0 -13
- package/.agent/skills/gstack-browse/test/fixtures/native-cookie-remove-fixture.cjs +0 -42
- package/.agent/skills/gstack-browse/test/fixtures/network-idle.html +0 -30
- package/.agent/skills/gstack-browse/test/fixtures/qa-eval-checkout.html +0 -108
- package/.agent/skills/gstack-browse/test/fixtures/qa-eval-spa.html +0 -98
- package/.agent/skills/gstack-browse/test/fixtures/qa-eval.html +0 -51
- package/.agent/skills/gstack-browse/test/fixtures/qa-only.html +0 -13
- package/.agent/skills/gstack-browse/test/fixtures/responsive.html +0 -49
- package/.agent/skills/gstack-browse/test/fixtures/route-dispatch-allowlist.json +0 -22
- package/.agent/skills/gstack-browse/test/fixtures/snapshot-ambiguous.html +0 -24
- package/.agent/skills/gstack-browse/test/fixtures/snapshot.html +0 -55
- package/.agent/skills/gstack-browse/test/fixtures/spa.html +0 -24
- package/.agent/skills/gstack-browse/test/fixtures/states.html +0 -17
- package/.agent/skills/gstack-browse/test/fixtures/upload-path-validation.ts +0 -91
- package/.agent/skills/gstack-browse/test/fixtures/upload.html +0 -25
- package/.agent/skills/gstack-browse/test/from-file-path-validation.test.ts +0 -68
- package/.agent/skills/gstack-browse/test/gstack-config.test.ts +0 -252
- package/.agent/skills/gstack-browse/test/gstack-update-check.test.ts +0 -614
- package/.agent/skills/gstack-browse/test/handoff.test.ts +0 -508
- package/.agent/skills/gstack-browse/test/headless-gpu-and-reap.test.ts +0 -286
- package/.agent/skills/gstack-browse/test/headless-launch-options.test.ts +0 -93
- package/.agent/skills/gstack-browse/test/launch-signal-flags.test.ts +0 -75
- package/.agent/skills/gstack-browse/test/learnings-injection.test.ts +0 -33
- package/.agent/skills/gstack-browse/test/media-extract-unit.test.ts +0 -294
- package/.agent/skills/gstack-browse/test/memory-command.test.ts +0 -219
- package/.agent/skills/gstack-browse/test/memory-leak-reproducer.test.ts +0 -133
- package/.agent/skills/gstack-browse/test/navigation-guard.test.ts +0 -106
- package/.agent/skills/gstack-browse/test/output-path-symlinks.test.ts +0 -76
- package/.agent/skills/gstack-browse/test/pair-agent-e2e.test.ts +0 -793
- package/.agent/skills/gstack-browse/test/pair-agent-optin-gate.test.ts +0 -171
- package/.agent/skills/gstack-browse/test/pair-agent-tunnel-eval.test.ts +0 -501
- package/.agent/skills/gstack-browse/test/path-security-realpath.test.ts +0 -89
- package/.agent/skills/gstack-browse/test/path-validation.test.ts +0 -268
- package/.agent/skills/gstack-browse/test/pdf-flags.test.ts +0 -86
- package/.agent/skills/gstack-browse/test/platform.test.ts +0 -37
- package/.agent/skills/gstack-browse/test/playwright-core-patch.test.ts +0 -62
- package/.agent/skills/gstack-browse/test/poisoned-bundle-probe.test.ts +0 -148
- package/.agent/skills/gstack-browse/test/process-liveness-windows.test.ts +0 -176
- package/.agent/skills/gstack-browse/test/proxy-config.test.ts +0 -189
- package/.agent/skills/gstack-browse/test/proxy-env-daemon.test.ts +0 -137
- package/.agent/skills/gstack-browse/test/proxy-env.test.ts +0 -189
- package/.agent/skills/gstack-browse/test/proxy-redact.test.ts +0 -64
- package/.agent/skills/gstack-browse/test/pty-inject-scan.test.ts +0 -153
- package/.agent/skills/gstack-browse/test/pty-session-lease.test.ts +0 -98
- package/.agent/skills/gstack-browse/test/rebrand-signed-bundle.test.ts +0 -53
- package/.agent/skills/gstack-browse/test/regression-pr1169-pdf-from-file-invalid-json.test.ts +0 -84
- package/.agent/skills/gstack-browse/test/restart-env.test.ts +0 -39
- package/.agent/skills/gstack-browse/test/route-test-harness.ts +0 -121
- package/.agent/skills/gstack-browse/test/runtime-root-server-bundle.test.ts +0 -100
- package/.agent/skills/gstack-browse/test/sanitize.test.ts +0 -112
- package/.agent/skills/gstack-browse/test/screenshot-size-guard.test.ts +0 -118
- package/.agent/skills/gstack-browse/test/security-adversarial-fixes.test.ts +0 -101
- package/.agent/skills/gstack-browse/test/security-adversarial.test.ts +0 -342
- package/.agent/skills/gstack-browse/test/security-audit-r2.test.ts +0 -432
- package/.agent/skills/gstack-browse/test/security-bench.test.ts +0 -159
- package/.agent/skills/gstack-browse/test/security-classifier-download-cleanup.test.ts +0 -138
- package/.agent/skills/gstack-browse/test/security-classifier.test.ts +0 -29
- package/.agent/skills/gstack-browse/test/security-integration.test.ts +0 -184
- package/.agent/skills/gstack-browse/test/security-live-playwright.test.ts +0 -195
- package/.agent/skills/gstack-browse/test/security-sidecar-client.test.ts +0 -66
- package/.agent/skills/gstack-browse/test/security.test.ts +0 -268
- package/.agent/skills/gstack-browse/test/server-auth.test.ts +0 -600
- package/.agent/skills/gstack-browse/test/server-embedder-terminal-port.test.ts +0 -301
- package/.agent/skills/gstack-browse/test/server-factory.test.ts +0 -640
- package/.agent/skills/gstack-browse/test/server-flush-trackers.test.ts +0 -73
- package/.agent/skills/gstack-browse/test/server-launch-resolution.test.ts +0 -49
- package/.agent/skills/gstack-browse/test/server-lock-errors.test.ts +0 -124
- package/.agent/skills/gstack-browse/test/server-no-import-side-effects.test.ts +0 -110
- package/.agent/skills/gstack-browse/test/server-proxy-fail-fast.test.ts +0 -98
- package/.agent/skills/gstack-browse/test/server-pty-lease-routes.test.ts +0 -156
- package/.agent/skills/gstack-browse/test/server-route-auth-blackbox.test.ts +0 -270
- package/.agent/skills/gstack-browse/test/server-route-dispatch-ratchet.test.ts +0 -129
- package/.agent/skills/gstack-browse/test/server-route-table.test.ts +0 -238
- package/.agent/skills/gstack-browse/test/server-sanitize-surrogates.test.ts +0 -182
- package/.agent/skills/gstack-browse/test/server-tmp-state-path.test.ts +0 -110
- package/.agent/skills/gstack-browse/test/session-cookie-store.test.ts +0 -159
- package/.agent/skills/gstack-browse/test/session-persist.test.ts +0 -287
- package/.agent/skills/gstack-browse/test/sidebar-tabs.test.ts +0 -268
- package/.agent/skills/gstack-browse/test/sidebar-ux.test.ts +0 -1006
- package/.agent/skills/gstack-browse/test/sidepanel-font-size.test.ts +0 -147
- package/.agent/skills/gstack-browse/test/sidepanel-patient-autoconnect.test.ts +0 -70
- package/.agent/skills/gstack-browse/test/sidepanel-reattach.test.ts +0 -93
- package/.agent/skills/gstack-browse/test/sidepanel-restart-dispose.test.ts +0 -106
- package/.agent/skills/gstack-browse/test/skill-token.test.ts +0 -167
- package/.agent/skills/gstack-browse/test/snapshot.test.ts +0 -593
- package/.agent/skills/gstack-browse/test/socks-bridge.test.ts +0 -461
- package/.agent/skills/gstack-browse/test/sse-helpers.test.ts +0 -194
- package/.agent/skills/gstack-browse/test/sse-session-cookie.test.ts +0 -155
- package/.agent/skills/gstack-browse/test/state-ttl.test.ts +0 -35
- package/.agent/skills/gstack-browse/test/stealth-extended.test.ts +0 -127
- package/.agent/skills/gstack-browse/test/stealth-layer-c.test.ts +0 -284
- package/.agent/skills/gstack-browse/test/stealth-webdriver.test.ts +0 -394
- package/.agent/skills/gstack-browse/test/stop-ack-before-shutdown.test.ts +0 -23
- package/.agent/skills/gstack-browse/test/stop-dead-daemon.test.ts +0 -153
- package/.agent/skills/gstack-browse/test/tab-each.test.ts +0 -196
- package/.agent/skills/gstack-browse/test/tab-guardrail.test.ts +0 -118
- package/.agent/skills/gstack-browse/test/tab-isolation.test.ts +0 -309
- package/.agent/skills/gstack-browse/test/tab-session-frame-detach.test.ts +0 -154
- package/.agent/skills/gstack-browse/test/telemetry-optout.test.ts +0 -171
- package/.agent/skills/gstack-browse/test/telemetry.test.ts +0 -77
- package/.agent/skills/gstack-browse/test/temp-dirs.test.ts +0 -129
- package/.agent/skills/gstack-browse/test/terminal-agent-detach-reattach.test.ts +0 -84
- package/.agent/skills/gstack-browse/test/terminal-agent-import.test.ts +0 -42
- package/.agent/skills/gstack-browse/test/terminal-agent-integration.test.ts +0 -565
- package/.agent/skills/gstack-browse/test/terminal-agent-keepalive.test.ts +0 -88
- package/.agent/skills/gstack-browse/test/terminal-agent-lifecycle.test.ts +0 -371
- package/.agent/skills/gstack-browse/test/terminal-agent-native-observation.test.ts +0 -66
- package/.agent/skills/gstack-browse/test/terminal-agent-owner-watchdog.test.ts +0 -81
- package/.agent/skills/gstack-browse/test/terminal-agent-pid-identity.test.ts +0 -161
- package/.agent/skills/gstack-browse/test/terminal-agent-port-range.test.ts +0 -80
- package/.agent/skills/gstack-browse/test/terminal-agent-publication-lock.test.ts +0 -289
- package/.agent/skills/gstack-browse/test/terminal-agent-ring-buffer-runtime.test.ts +0 -157
- package/.agent/skills/gstack-browse/test/terminal-agent-session-routing.test.ts +0 -96
- package/.agent/skills/gstack-browse/test/terminal-agent-watchdog.test.ts +0 -101
- package/.agent/skills/gstack-browse/test/terminal-agent.test.ts +0 -259
- package/.agent/skills/gstack-browse/test/terminal-pty-lifecycle.test.ts +0 -212
- package/.agent/skills/gstack-browse/test/test-server.ts +0 -57
- package/.agent/skills/gstack-browse/test/token-registry.test.ts +0 -615
- package/.agent/skills/gstack-browse/test/tunnel-gate-unit.test.ts +0 -129
- package/.agent/skills/gstack-browse/test/tunnel-revoke-cli.test.ts +0 -455
- package/.agent/skills/gstack-browse/test/url-validation.test.ts +0 -384
- package/.agent/skills/gstack-browse/test/watch.test.ts +0 -129
- package/.agent/skills/gstack-browse/test/watchdog.test.ts +0 -352
- package/.agent/skills/gstack-browse/test/welcome-page.test.ts +0 -143
- package/.agent/skills/gstack-browse/test/windows-spawn-hide.test.ts +0 -167
- package/.agent/skills/gstack-browse/test/xprotect-heal.test.ts +0 -457
- package/.agent/skills/gstack-browse/test/xvfb.test.ts +0 -603
- package/.agent/skills/gstack-canary/SKILL.md +0 -699
- package/.agent/skills/gstack-canary/SKILL.md.tmpl +0 -254
- package/.agent/skills/gstack-careful/SKILL.md +0 -96
- package/.agent/skills/gstack-careful/SKILL.md.tmpl +0 -107
- package/.agent/skills/gstack-careful/bin/check-careful.sh +0 -407
- package/.agent/skills/gstack-careful/bin/hook-extract.sh +0 -198
- package/.agent/skills/gstack-codex/SKILL.md +0 -889
- package/.agent/skills/gstack-codex/SKILL.md.tmpl +0 -358
- package/.agent/skills/gstack-codex/sections/challenge-mode.md +0 -150
- package/.agent/skills/gstack-codex/sections/challenge-mode.md.tmpl +0 -136
- package/.agent/skills/gstack-codex/sections/consult-mode.md +0 -221
- package/.agent/skills/gstack-codex/sections/consult-mode.md.tmpl +0 -207
- package/.agent/skills/gstack-codex/sections/manifest.json +0 -26
- package/.agent/skills/gstack-codex/sections/review-mode.md +0 -265
- package/.agent/skills/gstack-codex/sections/review-mode.md.tmpl +0 -246
- package/.agent/skills/gstack-context-restore/SKILL.md +0 -612
- package/.agent/skills/gstack-context-restore/SKILL.md.tmpl +0 -289
- package/.agent/skills/gstack-context-save/SKILL.md +0 -631
- package/.agent/skills/gstack-context-save/SKILL.md.tmpl +0 -307
- package/.agent/skills/gstack-cso/ACKNOWLEDGEMENTS.md +0 -17
- package/.agent/skills/gstack-cso/SKILL.md +0 -166
- package/.agent/skills/gstack-cso/SKILL.md.tmpl +0 -162
- package/.agent/skills/gstack-cso/sections/audit-phases.md +0 -116
- package/.agent/skills/gstack-cso/sections/audit-phases.md.tmpl +0 -114
- package/.agent/skills/gstack-cso/sections/manifest.json +0 -14
- package/.agent/skills/gstack-design-consultation/SKILL.md +0 -795
- package/.agent/skills/gstack-design-consultation/SKILL.md.tmpl +0 -234
- package/.agent/skills/gstack-design-consultation/sections/manifest.json +0 -14
- package/.agent/skills/gstack-design-consultation/sections/proposal-and-preview.md +0 -595
- package/.agent/skills/gstack-design-consultation/sections/proposal-and-preview.md.tmpl +0 -350
- package/.agent/skills/gstack-design-html/SKILL.md +0 -866
- package/.agent/skills/gstack-design-html/SKILL.md.tmpl +0 -476
- package/.agent/skills/gstack-design-html/sections/detector-install-offer.md +0 -40
- package/.agent/skills/gstack-design-html/sections/detector-install-offer.md.tmpl +0 -6
- package/.agent/skills/gstack-design-html/sections/doctrine.md +0 -86
- package/.agent/skills/gstack-design-html/sections/doctrine.md.tmpl +0 -1
- package/.agent/skills/gstack-design-html/sections/manifest.json +0 -26
- package/.agent/skills/gstack-design-html/sections/pretext-patterns.md +0 -145
- package/.agent/skills/gstack-design-html/sections/pretext-patterns.md.tmpl +0 -143
- package/.agent/skills/gstack-design-html/vendor/pretext.js +0 -5
- package/.agent/skills/gstack-design-review/SKILL.md +0 -1957
- package/.agent/skills/gstack-design-review/SKILL.md.tmpl +0 -344
- package/.agent/skills/gstack-design-shotgun/SKILL.md +0 -885
- package/.agent/skills/gstack-design-shotgun/SKILL.md.tmpl +0 -310
- package/.agent/skills/gstack-design-shotgun/sections/doctrine.md +0 -86
- package/.agent/skills/gstack-design-shotgun/sections/doctrine.md.tmpl +0 -1
- package/.agent/skills/gstack-design-shotgun/sections/manifest.json +0 -14
- package/.agent/skills/gstack-deslop-shared-libs/SKILL.md +0 -209
- package/.agent/skills/gstack-deslop-shared-libs/SKILL.md.tmpl +0 -181
- package/.agent/skills/gstack-devex-review/SKILL.md +0 -1107
- package/.agent/skills/gstack-devex-review/SKILL.md.tmpl +0 -241
- package/.agent/skills/gstack-diagram/SKILL.md +0 -295
- package/.agent/skills/gstack-diagram/SKILL.md.tmpl +0 -175
- package/.agent/skills/gstack-document-generate/SKILL.md +0 -814
- package/.agent/skills/gstack-document-generate/SKILL.md.tmpl +0 -460
- package/.agent/skills/gstack-document-release/SKILL.md +0 -557
- package/.agent/skills/gstack-document-release/SKILL.md.tmpl +0 -188
- package/.agent/skills/gstack-document-release/sections/audit-scope.md +0 -51
- package/.agent/skills/gstack-document-release/sections/audit-scope.md.tmpl +0 -49
- package/.agent/skills/gstack-document-release/sections/manifest.json +0 -20
- package/.agent/skills/gstack-document-release/sections/release-body.md +0 -640
- package/.agent/skills/gstack-document-release/sections/release-body.md.tmpl +0 -428
- package/.agent/skills/gstack-eval-plan/SKILL.md +0 -472
- package/.agent/skills/gstack-eval-plan/SKILL.md.tmpl +0 -157
- package/.agent/skills/gstack-freeze/SKILL.md +0 -79
- package/.agent/skills/gstack-freeze/SKILL.md.tmpl +0 -96
- package/.agent/skills/gstack-freeze/bin/check-freeze.sh +0 -210
- package/.agent/skills/gstack-freeze/bin/freeze-state.sh +0 -68
- package/.agent/skills/gstack-gstack-upgrade/SKILL.md +0 -410
- package/.agent/skills/gstack-gstack-upgrade/SKILL.md.tmpl +0 -413
- package/.agent/skills/gstack-gstack-upgrade/migrations/v0.15.2.0.sh +0 -20
- package/.agent/skills/gstack-gstack-upgrade/migrations/v0.16.2.0.sh +0 -56
- package/.agent/skills/gstack-gstack-upgrade/migrations/v1.0.0.0.sh +0 -40
- package/.agent/skills/gstack-gstack-upgrade/migrations/v1.1.3.0.sh +0 -137
- package/.agent/skills/gstack-gstack-upgrade/migrations/v1.17.0.0.sh +0 -59
- package/.agent/skills/gstack-gstack-upgrade/migrations/v1.27.0.0.sh +0 -432
- package/.agent/skills/gstack-gstack-upgrade/migrations/v1.37.0.0.sh +0 -104
- package/.agent/skills/gstack-gstack-upgrade/migrations/v1.38.1.0.sh +0 -104
- package/.agent/skills/gstack-gstack-upgrade/migrations/v1.40.0.0.sh +0 -144
- package/.agent/skills/gstack-gstack-upgrade/migrations/v1.58.0.0.sh +0 -65
- package/.agent/skills/gstack-gstack-upgrade/migrations/v1.65.0.0.sh +0 -152
- package/.agent/skills/gstack-gstack-upgrade/migrations/v1.67.0.0.sh +0 -73
- package/.agent/skills/gstack-gstack-upgrade/migrations/v1.78.0.0.sh +0 -30
- package/.agent/skills/gstack-gstack-upgrade/migrations/v1.86.0.0.sh +0 -12
- package/.agent/skills/gstack-gstack-upgrade/migrations/v1.91.19.0.sh +0 -53
- package/.agent/skills/gstack-gstack-upgrade/migrations/v1.91.65.0.sh +0 -31
- package/.agent/skills/gstack-guard/SKILL.md +0 -60
- package/.agent/skills/gstack-guard/SKILL.md.tmpl +0 -88
- package/.agent/skills/gstack-health/SKILL.md +0 -710
- package/.agent/skills/gstack-health/SKILL.md.tmpl +0 -386
- package/.agent/skills/gstack-investigate/SKILL.md +0 -706
- package/.agent/skills/gstack-investigate/SKILL.md.tmpl +0 -305
- package/.agent/skills/gstack-investigate/sections/backlog.md +0 -31
- package/.agent/skills/gstack-investigate/sections/backlog.md.tmpl +0 -29
- package/.agent/skills/gstack-investigate/sections/eval-bisect.md +0 -25
- package/.agent/skills/gstack-investigate/sections/eval-bisect.md.tmpl +0 -23
- package/.agent/skills/gstack-investigate/sections/flake.md +0 -50
- package/.agent/skills/gstack-investigate/sections/flake.md.tmpl +0 -43
- package/.agent/skills/gstack-investigate/sections/manifest.json +0 -38
- package/.agent/skills/gstack-investigate/sections/polluter.md +0 -33
- package/.agent/skills/gstack-investigate/sections/polluter.md.tmpl +0 -31
- package/.agent/skills/gstack-investigate/sections/race.md +0 -27
- package/.agent/skills/gstack-investigate/sections/race.md.tmpl +0 -25
- package/.agent/skills/gstack-ios-clean/SKILL.md +0 -427
- package/.agent/skills/gstack-ios-clean/SKILL.md.tmpl +0 -107
- package/.agent/skills/gstack-ios-design-review/SKILL.md +0 -434
- package/.agent/skills/gstack-ios-design-review/SKILL.md.tmpl +0 -111
- package/.agent/skills/gstack-ios-fix/SKILL.md +0 -423
- package/.agent/skills/gstack-ios-fix/SKILL.md.tmpl +0 -103
- package/.agent/skills/gstack-ios-qa/SKILL.md +0 -643
- package/.agent/skills/gstack-ios-qa/SKILL.md.tmpl +0 -297
- package/.agent/skills/gstack-ios-qa/daemon/src/allowlist.ts +0 -114
- package/.agent/skills/gstack-ios-qa/daemon/src/audit.ts +0 -97
- package/.agent/skills/gstack-ios-qa/daemon/src/auth-mint.ts +0 -89
- package/.agent/skills/gstack-ios-qa/daemon/src/cli-mint.ts +0 -149
- package/.agent/skills/gstack-ios-qa/daemon/src/devicectl.ts +0 -335
- package/.agent/skills/gstack-ios-qa/daemon/src/index.ts +0 -697
- package/.agent/skills/gstack-ios-qa/daemon/src/proxy.ts +0 -160
- package/.agent/skills/gstack-ios-qa/daemon/src/session-tokens.ts +0 -126
- package/.agent/skills/gstack-ios-qa/daemon/src/single-instance.ts +0 -171
- package/.agent/skills/gstack-ios-qa/daemon/src/tailscale-localapi.ts +0 -120
- package/.agent/skills/gstack-ios-qa/daemon/src/tunnel-bootstrap.ts +0 -459
- package/.agent/skills/gstack-ios-qa/daemon/src/types.ts +0 -91
- package/.agent/skills/gstack-ios-qa/daemon/test/allowlist.test.ts +0 -146
- package/.agent/skills/gstack-ios-qa/daemon/test/audit.test.ts +0 -111
- package/.agent/skills/gstack-ios-qa/daemon/test/auth-mint.test.ts +0 -106
- package/.agent/skills/gstack-ios-qa/daemon/test/cli-mint.test.ts +0 -120
- package/.agent/skills/gstack-ios-qa/daemon/test/daemon-integration.test.ts +0 -855
- package/.agent/skills/gstack-ios-qa/daemon/test/fake-device.ts +0 -238
- package/.agent/skills/gstack-ios-qa/daemon/test/proxy-classify.test.ts +0 -47
- package/.agent/skills/gstack-ios-qa/daemon/test/proxy-transport.test.ts +0 -499
- package/.agent/skills/gstack-ios-qa/daemon/test/session-tokens.test.ts +0 -156
- package/.agent/skills/gstack-ios-qa/daemon/test/single-instance.test.ts +0 -96
- package/.agent/skills/gstack-ios-qa/daemon/test/tailscale-localapi.test.ts +0 -55
- package/.agent/skills/gstack-ios-qa/daemon/test/tunnel-bootstrap.test.ts +0 -1030
- package/.agent/skills/gstack-ios-qa/templates/Bridges.swift.template +0 -474
- package/.agent/skills/gstack-ios-qa/templates/DebugBridgeManager.swift.template +0 -38
- package/.agent/skills/gstack-ios-qa/templates/DebugBridgeTouch.h.template +0 -38
- package/.agent/skills/gstack-ios-qa/templates/DebugBridgeTouch.m.template +0 -365
- package/.agent/skills/gstack-ios-qa/templates/DebugBridgeWiring.swift.template +0 -59
- package/.agent/skills/gstack-ios-qa/templates/DebugOverlay.swift.template +0 -137
- package/.agent/skills/gstack-ios-qa/templates/Package.swift.template +0 -82
- package/.agent/skills/gstack-ios-qa/templates/StateAccessor.swift.template +0 -41
- package/.agent/skills/gstack-ios-qa/templates/StateServer.swift.template +0 -625
- package/.agent/skills/gstack-ios-sync/SKILL.md +0 -418
- package/.agent/skills/gstack-ios-sync/SKILL.md.tmpl +0 -99
- package/.agent/skills/gstack-land-and-deploy/SKILL.md +0 -1028
- package/.agent/skills/gstack-land-and-deploy/SKILL.md.tmpl +0 -509
- package/.agent/skills/gstack-land-and-deploy/sections/first-run-validation.md +0 -188
- package/.agent/skills/gstack-land-and-deploy/sections/first-run-validation.md.tmpl +0 -150
- package/.agent/skills/gstack-land-and-deploy/sections/manifest.json +0 -26
- package/.agent/skills/gstack-land-and-deploy/sections/merge-and-deploy.md +0 -250
- package/.agent/skills/gstack-land-and-deploy/sections/merge-and-deploy.md.tmpl +0 -248
- package/.agent/skills/gstack-land-and-deploy/sections/readiness-gate.md +0 -308
- package/.agent/skills/gstack-land-and-deploy/sections/readiness-gate.md.tmpl +0 -306
- package/.agent/skills/gstack-landing-report/SKILL.md +0 -488
- package/.agent/skills/gstack-landing-report/SKILL.md.tmpl +0 -164
- package/.agent/skills/gstack-learn/SKILL.md +0 -519
- package/.agent/skills/gstack-learn/SKILL.md.tmpl +0 -197
- package/.agent/skills/gstack-make-pdf/SKILL.md +0 -409
- package/.agent/skills/gstack-make-pdf/SKILL.md.tmpl +0 -253
- package/.agent/skills/gstack-make-pdf/src/asideClient.ts +0 -114
- package/.agent/skills/gstack-make-pdf/src/cli.ts +0 -294
- package/.agent/skills/gstack-make-pdf/src/commands.ts +0 -62
- package/.agent/skills/gstack-make-pdf/src/diagram-prepass.ts +0 -834
- package/.agent/skills/gstack-make-pdf/src/html-to-docx.d.ts +0 -7
- package/.agent/skills/gstack-make-pdf/src/image-policy.ts +0 -236
- package/.agent/skills/gstack-make-pdf/src/image-size.ts +0 -117
- package/.agent/skills/gstack-make-pdf/src/orchestrator.ts +0 -339
- package/.agent/skills/gstack-make-pdf/src/pdftotext.ts +0 -318
- package/.agent/skills/gstack-make-pdf/src/print-css.ts +0 -452
- package/.agent/skills/gstack-make-pdf/src/render.ts +0 -636
- package/.agent/skills/gstack-make-pdf/src/setup.ts +0 -113
- package/.agent/skills/gstack-make-pdf/src/smartypants.ts +0 -105
- package/.agent/skills/gstack-make-pdf/src/toc-pages.ts +0 -197
- package/.agent/skills/gstack-make-pdf/src/types.ts +0 -109
- package/.agent/skills/gstack-make-pdf/test/asideClient.test.ts +0 -153
- package/.agent/skills/gstack-make-pdf/test/cli-args.test.ts +0 -94
- package/.agent/skills/gstack-make-pdf/test/cli-exit-codes.test.ts +0 -329
- package/.agent/skills/gstack-make-pdf/test/diagram-prepass.test.ts +0 -743
- package/.agent/skills/gstack-make-pdf/test/e2e/ci-prereqs.test.ts +0 -48
- package/.agent/skills/gstack-make-pdf/test/e2e/combined-gate.test.ts +0 -74
- package/.agent/skills/gstack-make-pdf/test/e2e/diagram-gate.test.ts +0 -173
- package/.agent/skills/gstack-make-pdf/test/e2e/emoji-gate.test.ts +0 -191
- package/.agent/skills/gstack-make-pdf/test/e2e/format-gate.test.ts +0 -130
- package/.agent/skills/gstack-make-pdf/test/e2e/landscape-gate.test.ts +0 -150
- package/.agent/skills/gstack-make-pdf/test/e2e/toc-gate.test.ts +0 -143
- package/.agent/skills/gstack-make-pdf/test/fixtures/combined-gate.expected.txt +0 -20
- package/.agent/skills/gstack-make-pdf/test/fixtures/combined-gate.md +0 -30
- package/.agent/skills/gstack-make-pdf/test/fixtures/diagram-assets/huge-noise.png +0 -0
- package/.agent/skills/gstack-make-pdf/test/fixtures/diagram-assets/red-box.png +0 -0
- package/.agent/skills/gstack-make-pdf/test/fixtures/diagram-assets/wide-arch.png +0 -0
- package/.agent/skills/gstack-make-pdf/test/fixtures/diagram-assets/wide-screenshot.png +0 -0
- package/.agent/skills/gstack-make-pdf/test/fixtures/diagram-gate.md +0 -48
- package/.agent/skills/gstack-make-pdf/test/fixtures/emoji-gate.md +0 -12
- package/.agent/skills/gstack-make-pdf/test/fixtures/landscape-gate.md +0 -52
- package/.agent/skills/gstack-make-pdf/test/image-policy.test.ts +0 -215
- package/.agent/skills/gstack-make-pdf/test/pdftotext.test.ts +0 -327
- package/.agent/skills/gstack-make-pdf/test/render-offline-sanitize.test.ts +0 -268
- package/.agent/skills/gstack-make-pdf/test/render.test.ts +0 -603
- package/.agent/skills/gstack-make-pdf/test/setup-smoke.test.ts +0 -259
- package/.agent/skills/gstack-make-pdf/test/toc-anchors.test.ts +0 -95
- package/.agent/skills/gstack-make-pdf/test/toc-pages.test.ts +0 -148
- package/.agent/skills/gstack-office-hours/SKILL.md +0 -1324
- package/.agent/skills/gstack-office-hours/SKILL.md.tmpl +0 -372
- package/.agent/skills/gstack-office-hours/sections/design-and-handoff.md +0 -706
- package/.agent/skills/gstack-office-hours/sections/design-and-handoff.md.tmpl +0 -547
- package/.agent/skills/gstack-office-hours/sections/manifest.json +0 -26
- package/.agent/skills/gstack-office-hours/sections/phase-2a-startup-diagnostic.md +0 -163
- package/.agent/skills/gstack-office-hours/sections/phase-2a-startup-diagnostic.md.tmpl +0 -161
- package/.agent/skills/gstack-office-hours/sections/phase-2b-builder-brainstorm.md +0 -43
- package/.agent/skills/gstack-office-hours/sections/phase-2b-builder-brainstorm.md.tmpl +0 -41
- package/.agent/skills/gstack-open-gstack-browser/SKILL.md +0 -414
- package/.agent/skills/gstack-open-gstack-browser/SKILL.md.tmpl +0 -248
- package/.agent/skills/gstack-pair-agent/SKILL.md +0 -730
- package/.agent/skills/gstack-pair-agent/SKILL.md.tmpl +0 -366
- package/.agent/skills/gstack-plan-ceo-review/SKILL.md +0 -1245
- package/.agent/skills/gstack-plan-ceo-review/SKILL.md.tmpl +0 -596
- package/.agent/skills/gstack-plan-ceo-review/bin/mode-handoff-hook +0 -5
- package/.agent/skills/gstack-plan-ceo-review/bin/mode-handoff-hook.ts +0 -34
- package/.agent/skills/gstack-plan-ceo-review/sections/checklist.md +0 -37
- package/.agent/skills/gstack-plan-ceo-review/sections/checklist.md.tmpl +0 -35
- package/.agent/skills/gstack-plan-ceo-review/sections/manifest.json +0 -23
- package/.agent/skills/gstack-plan-ceo-review/sections/review-sections.md +0 -1207
- package/.agent/skills/gstack-plan-ceo-review/sections/review-sections.md.tmpl +0 -605
- package/.agent/skills/gstack-plan-design-review/SKILL.md +0 -1230
- package/.agent/skills/gstack-plan-design-review/SKILL.md.tmpl +0 -344
- package/.agent/skills/gstack-plan-design-review/sections/checklist.md +0 -27
- package/.agent/skills/gstack-plan-design-review/sections/checklist.md.tmpl +0 -25
- package/.agent/skills/gstack-plan-design-review/sections/manifest.json +0 -23
- package/.agent/skills/gstack-plan-design-review/sections/review-sections.md +0 -727
- package/.agent/skills/gstack-plan-design-review/sections/review-sections.md.tmpl +0 -281
- package/.agent/skills/gstack-plan-devex-review/SKILL.md +0 -1072
- package/.agent/skills/gstack-plan-devex-review/SKILL.md.tmpl +0 -459
- package/.agent/skills/gstack-plan-devex-review/dx-hall-of-fame.md +0 -127
- package/.agent/skills/gstack-plan-devex-review/sections/checklist.md +0 -33
- package/.agent/skills/gstack-plan-devex-review/sections/checklist.md.tmpl +0 -31
- package/.agent/skills/gstack-plan-devex-review/sections/manifest.json +0 -23
- package/.agent/skills/gstack-plan-devex-review/sections/review-sections.md +0 -1037
- package/.agent/skills/gstack-plan-devex-review/sections/review-sections.md.tmpl +0 -446
- package/.agent/skills/gstack-plan-eng-review/SKILL.md +0 -702
- package/.agent/skills/gstack-plan-eng-review/SKILL.md.tmpl +0 -191
- package/.agent/skills/gstack-plan-eng-review/sections/checklist.md +0 -34
- package/.agent/skills/gstack-plan-eng-review/sections/checklist.md.tmpl +0 -32
- package/.agent/skills/gstack-plan-eng-review/sections/manifest.json +0 -23
- package/.agent/skills/gstack-plan-eng-review/sections/review-sections.md +0 -1545
- package/.agent/skills/gstack-plan-eng-review/sections/review-sections.md.tmpl +0 -614
- package/.agent/skills/gstack-plan-tune/SKILL.md +0 -986
- package/.agent/skills/gstack-plan-tune/SKILL.md.tmpl +0 -669
- package/.agent/skills/gstack-qa/SKILL.md +0 -781
- package/.agent/skills/gstack-qa/SKILL.md.tmpl +0 -305
- package/.agent/skills/gstack-qa/references/issue-taxonomy.md +0 -85
- package/.agent/skills/gstack-qa/sections/browser-setup.md +0 -114
- package/.agent/skills/gstack-qa/sections/browser-setup.md.tmpl +0 -28
- package/.agent/skills/gstack-qa/sections/browser-verify.md +0 -19
- package/.agent/skills/gstack-qa/sections/browser-verify.md.tmpl +0 -17
- package/.agent/skills/gstack-qa/sections/exploratory.md +0 -92
- package/.agent/skills/gstack-qa/sections/exploratory.md.tmpl +0 -1
- package/.agent/skills/gstack-qa/sections/manifest.json +0 -50
- package/.agent/skills/gstack-qa/sections/qa-patterns.md +0 -226
- package/.agent/skills/gstack-qa/sections/qa-patterns.md.tmpl +0 -1
- package/.agent/skills/gstack-qa/sections/scope.md +0 -23
- package/.agent/skills/gstack-qa/sections/scope.md.tmpl +0 -1
- package/.agent/skills/gstack-qa/sections/system-functional.md +0 -61
- package/.agent/skills/gstack-qa/sections/system-functional.md.tmpl +0 -1
- package/.agent/skills/gstack-qa/sections/test-bootstrap.md +0 -86
- package/.agent/skills/gstack-qa/sections/test-bootstrap.md.tmpl +0 -74
- package/.agent/skills/gstack-qa/templates/functional-report-template.md +0 -55
- package/.agent/skills/gstack-qa/templates/qa-report-template.md +0 -124
- package/.agent/skills/gstack-qa-only/SKILL.md +0 -595
- package/.agent/skills/gstack-qa-only/SKILL.md.tmpl +0 -214
- package/.agent/skills/gstack-qa-only/sections/exploratory.md +0 -107
- package/.agent/skills/gstack-qa-only/sections/exploratory.md.tmpl +0 -1
- package/.agent/skills/gstack-qa-only/sections/manifest.json +0 -19
- package/.agent/skills/gstack-qa-only/sections/reporting.md +0 -78
- package/.agent/skills/gstack-qa-only/sections/reporting.md.tmpl +0 -76
- package/.agent/skills/gstack-retro/SKILL.md +0 -1178
- package/.agent/skills/gstack-retro/SKILL.md.tmpl +0 -769
- package/.agent/skills/gstack-retro/sections/manifest.json +0 -14
- package/.agent/skills/gstack-retro/sections/report-format.md +0 -127
- package/.agent/skills/gstack-retro/sections/report-format.md.tmpl +0 -125
- package/.agent/skills/gstack-review/SKILL.md +0 -1199
- package/.agent/skills/gstack-review/SKILL.md.tmpl +0 -482
- package/.agent/skills/gstack-review/TODOS-format.md +0 -61
- package/.agent/skills/gstack-review/checklist.md +0 -222
- package/.agent/skills/gstack-review/design-checklist.md +0 -193
- package/.agent/skills/gstack-review/gate-integrity.md +0 -171
- package/.agent/skills/gstack-review/greptile-triage.md +0 -261
- package/.agent/skills/gstack-review/sections/adversarial.md +0 -296
- package/.agent/skills/gstack-review/sections/adversarial.md.tmpl +0 -1
- package/.agent/skills/gstack-review/sections/manifest.json +0 -32
- package/.agent/skills/gstack-review/sections/plan-completion.md +0 -232
- package/.agent/skills/gstack-review/sections/plan-completion.md.tmpl +0 -3
- package/.agent/skills/gstack-review/sections/review-army.md +0 -263
- package/.agent/skills/gstack-review/sections/review-army.md.tmpl +0 -1
- package/.agent/skills/gstack-review/sections/shared-code-reuse.md +0 -34
- package/.agent/skills/gstack-review/sections/shared-code-reuse.md.tmpl +0 -1
- package/.agent/skills/gstack-review/specialists/api-contract.md +0 -49
- package/.agent/skills/gstack-review/specialists/data-migration.md +0 -48
- package/.agent/skills/gstack-review/specialists/maintainability.md +0 -46
- package/.agent/skills/gstack-review/specialists/performance.md +0 -52
- package/.agent/skills/gstack-review/specialists/red-team.md +0 -45
- package/.agent/skills/gstack-review/specialists/security.md +0 -65
- package/.agent/skills/gstack-review/specialists/simplification.md +0 -49
- package/.agent/skills/gstack-review/specialists/testing.md +0 -160
- package/.agent/skills/gstack-scrape/SKILL.md +0 -431
- package/.agent/skills/gstack-scrape/SKILL.md.tmpl +0 -187
- package/.agent/skills/gstack-setup-browser-cookies/SKILL.md +0 -231
- package/.agent/skills/gstack-setup-browser-cookies/SKILL.md.tmpl +0 -64
- package/.agent/skills/gstack-setup-deploy/SKILL.md +0 -605
- package/.agent/skills/gstack-setup-deploy/SKILL.md.tmpl +0 -250
- package/.agent/skills/gstack-setup-gbrain/SKILL.md +0 -1074
- package/.agent/skills/gstack-setup-gbrain/SKILL.md.tmpl +0 -742
- package/.agent/skills/gstack-setup-gbrain/memory.md +0 -426
- package/.agent/skills/gstack-setup-gbrain/sections/brain-init.md +0 -269
- package/.agent/skills/gstack-setup-gbrain/sections/brain-init.md.tmpl +0 -267
- package/.agent/skills/gstack-setup-gbrain/sections/claude-md-persist.md +0 -40
- package/.agent/skills/gstack-setup-gbrain/sections/claude-md-persist.md.tmpl +0 -38
- package/.agent/skills/gstack-setup-gbrain/sections/engine-remediation.md +0 -71
- package/.agent/skills/gstack-setup-gbrain/sections/engine-remediation.md.tmpl +0 -69
- package/.agent/skills/gstack-setup-gbrain/sections/manifest.json +0 -32
- package/.agent/skills/gstack-setup-gbrain/sections/transcript-gate.md +0 -105
- package/.agent/skills/gstack-setup-gbrain/sections/transcript-gate.md.tmpl +0 -103
- package/.agent/skills/gstack-ship/SKILL.md +0 -1220
- package/.agent/skills/gstack-ship/SKILL.md.tmpl +0 -694
- package/.agent/skills/gstack-ship/sections/adversarial.md +0 -362
- package/.agent/skills/gstack-ship/sections/adversarial.md.tmpl +0 -22
- package/.agent/skills/gstack-ship/sections/apple-release.md +0 -98
- package/.agent/skills/gstack-ship/sections/apple-release.md.tmpl +0 -96
- package/.agent/skills/gstack-ship/sections/changelog.md +0 -47
- package/.agent/skills/gstack-ship/sections/changelog.md.tmpl +0 -5
- package/.agent/skills/gstack-ship/sections/documentation.md +0 -118
- package/.agent/skills/gstack-ship/sections/documentation.md.tmpl +0 -116
- package/.agent/skills/gstack-ship/sections/greptile-early.md +0 -57
- package/.agent/skills/gstack-ship/sections/greptile-early.md.tmpl +0 -48
- package/.agent/skills/gstack-ship/sections/greptile.md +0 -95
- package/.agent/skills/gstack-ship/sections/greptile.md.tmpl +0 -93
- package/.agent/skills/gstack-ship/sections/handoff.md +0 -22
- package/.agent/skills/gstack-ship/sections/handoff.md.tmpl +0 -20
- package/.agent/skills/gstack-ship/sections/manifest.json +0 -92
- package/.agent/skills/gstack-ship/sections/measure.md +0 -66
- package/.agent/skills/gstack-ship/sections/measure.md.tmpl +0 -64
- package/.agent/skills/gstack-ship/sections/plan-completion.md +0 -387
- package/.agent/skills/gstack-ship/sections/plan-completion.md.tmpl +0 -67
- package/.agent/skills/gstack-ship/sections/pr-body.md +0 -258
- package/.agent/skills/gstack-ship/sections/pr-body.md.tmpl +0 -241
- package/.agent/skills/gstack-ship/sections/review-army.md +0 -728
- package/.agent/skills/gstack-ship/sections/review-army.md.tmpl +0 -171
- package/.agent/skills/gstack-ship/sections/shared-code-reuse.md +0 -34
- package/.agent/skills/gstack-ship/sections/shared-code-reuse.md.tmpl +0 -1
- package/.agent/skills/gstack-ship/sections/test-coverage.md +0 -478
- package/.agent/skills/gstack-ship/sections/test-coverage.md.tmpl +0 -118
- package/.agent/skills/gstack-ship/sections/tests.md +0 -453
- package/.agent/skills/gstack-ship/sections/tests.md.tmpl +0 -144
- package/.agent/skills/gstack-skillify/SKILL.md +0 -790
- package/.agent/skills/gstack-skillify/SKILL.md.tmpl +0 -437
- package/.agent/skills/gstack-spec/SKILL.md +0 -893
- package/.agent/skills/gstack-spec/SKILL.md.tmpl +0 -498
- package/.agent/skills/gstack-spec/sections/gate-and-file.md +0 -470
- package/.agent/skills/gstack-spec/sections/gate-and-file.md.tmpl +0 -319
- package/.agent/skills/gstack-spec/sections/manifest.json +0 -14
- package/.agent/skills/gstack-sync-gbrain/SKILL.md +0 -935
- package/.agent/skills/gstack-sync-gbrain/SKILL.md.tmpl +0 -615
- package/.agent/skills/gstack-test-audit/SKILL.md +0 -500
- package/.agent/skills/gstack-test-audit/SKILL.md.tmpl +0 -149
- package/.agent/skills/gstack-unfreeze/SKILL.md +0 -39
- package/.agent/skills/gstack-unfreeze/SKILL.md.tmpl +0 -39
- package/.agent/skills/gstack-upgrade/SKILL.md +0 -410
- package/.agent/skills/gstack-upgrade/SKILL.md.tmpl +0 -413
- package/.agent/skills/gstack-upgrade/migrations/v0.15.2.0.sh +0 -20
- package/.agent/skills/gstack-upgrade/migrations/v0.16.2.0.sh +0 -56
- package/.agent/skills/gstack-upgrade/migrations/v1.0.0.0.sh +0 -40
- package/.agent/skills/gstack-upgrade/migrations/v1.1.3.0.sh +0 -137
- package/.agent/skills/gstack-upgrade/migrations/v1.17.0.0.sh +0 -59
- package/.agent/skills/gstack-upgrade/migrations/v1.27.0.0.sh +0 -432
- package/.agent/skills/gstack-upgrade/migrations/v1.37.0.0.sh +0 -104
- package/.agent/skills/gstack-upgrade/migrations/v1.38.1.0.sh +0 -104
- package/.agent/skills/gstack-upgrade/migrations/v1.40.0.0.sh +0 -144
- package/.agent/skills/gstack-upgrade/migrations/v1.58.0.0.sh +0 -65
- package/.agent/skills/gstack-upgrade/migrations/v1.65.0.0.sh +0 -152
- package/.agent/skills/gstack-upgrade/migrations/v1.67.0.0.sh +0 -73
- package/.agent/skills/gstack-upgrade/migrations/v1.78.0.0.sh +0 -30
- package/.agent/skills/gstack-upgrade/migrations/v1.86.0.0.sh +0 -12
- package/.agent/skills/gstack-upgrade/migrations/v1.91.19.0.sh +0 -53
- package/.agent/skills/gstack-upgrade/migrations/v1.91.65.0.sh +0 -31
- package/.agent/skills/html-renderer/SKILL.md +0 -336
- package/.agent/skills/html-renderer/references/animation-patterns.md +0 -609
- package/.agent/skills/html-renderer/references/character-animation.md +0 -388
- package/.agent/skills/html-renderer/references/collage-broll.md +0 -41
- package/.agent/skills/html-renderer/references/design.md +0 -217
- package/.agent/skills/html-renderer/references/directing.md +0 -165
- package/.agent/skills/html-renderer/references/earshot-audio.md +0 -201
- package/.agent/skills/html-renderer/references/engine-api.md +0 -480
- package/.agent/skills/html-renderer/references/film-styles.md +0 -95
- package/.agent/skills/html-renderer/references/hyperframes-motion.md +0 -208
- package/.agent/skills/html-renderer/references/motion-craft.md +0 -92
- package/.agent/skills/html-renderer/references/onetake-camera.md +0 -59
- package/.agent/skills/html-renderer/references/procedural-sfx.md +0 -147
- package/.agent/skills/html-renderer/references/remotion-patterns.md +0 -159
- package/.agent/skills/html-renderer/references/render-contract.md +0 -66
- package/.agent/skills/motion-styles/LIBRARY.md +0 -896
- package/.agent/skills/motion-styles/README.md +0 -29
- package/.agent/skills/motion-styles/SKILL.md +0 -70
- package/.agent/skills/motion-styles/catalog.json +0 -2253
- package/.agent/skills/motion-styles/prompts/bio-glow.md +0 -149
- package/.agent/skills/motion-styles/prompts/blueprint.md +0 -145
- package/.agent/skills/motion-styles/prompts/breaking-desk.md +0 -147
- package/.agent/skills/motion-styles/prompts/callout-kit.md +0 -147
- package/.agent/skills/motion-styles/prompts/chalk-talk.md +0 -153
- package/.agent/skills/motion-styles/prompts/chat-story.md +0 -143
- package/.agent/skills/motion-styles/prompts/clay-diorama.md +0 -149
- package/.agent/skills/motion-styles/prompts/clean-tech.md +0 -147
- package/.agent/skills/motion-styles/prompts/comic-panels.md +0 -145
- package/.agent/skills/motion-styles/prompts/cosmic-scale.md +0 -147
- package/.agent/skills/motion-styles/prompts/creator-motion.md +0 -149
- package/.agent/skills/motion-styles/prompts/data-city.md +0 -149
- package/.agent/skills/motion-styles/prompts/data-story.md +0 -147
- package/.agent/skills/motion-styles/prompts/deep-layers.md +0 -147
- package/.agent/skills/motion-styles/prompts/doc-cinema.md +0 -145
- package/.agent/skills/motion-styles/prompts/evidence-board.md +0 -145
- package/.agent/skills/motion-styles/prompts/exploded-view.md +0 -149
- package/.agent/skills/motion-styles/prompts/glass-icons.md +0 -147
- package/.agent/skills/motion-styles/prompts/glitch-pack.md +0 -145
- package/.agent/skills/motion-styles/prompts/globe-arcs.md +0 -147
- package/.agent/skills/motion-styles/prompts/headline-backdrop.md +0 -145
- package/.agent/skills/motion-styles/prompts/iso-cutaway.md +0 -147
- package/.agent/skills/motion-styles/prompts/isotype.md +0 -145
- package/.agent/skills/motion-styles/prompts/karaoke-subs.md +0 -145
- package/.agent/skills/motion-styles/prompts/low-poly-atlas.md +0 -149
- package/.agent/skills/motion-styles/prompts/lower-thirds.md +0 -147
- package/.agent/skills/motion-styles/prompts/marker-board.md +0 -145
- package/.agent/skills/motion-styles/prompts/market-tape.md +0 -145
- package/.agent/skills/motion-styles/prompts/math-canvas.md +0 -145
- package/.agent/skills/motion-styles/prompts/myth-or-fact.md +0 -145
- package/.agent/skills/motion-styles/prompts/neon-line.md +0 -145
- package/.agent/skills/motion-styles/prompts/paper-craft.md +0 -145
- package/.agent/skills/motion-styles/prompts/parchment-map.md +0 -147
- package/.agent/skills/motion-styles/prompts/physics-toybox.md +0 -147
- package/.agent/skills/motion-styles/prompts/pixel-quest.md +0 -145
- package/.agent/skills/motion-styles/prompts/podcast-clip.md +0 -147
- package/.agent/skills/motion-styles/prompts/pop-info.md +0 -145
- package/.agent/skills/motion-styles/prompts/product-turntable.md +0 -149
- package/.agent/skills/motion-styles/prompts/punch-captions.md +0 -145
- package/.agent/skills/motion-styles/prompts/quiz-countdown.md +0 -145
- package/.agent/skills/motion-styles/prompts/quote-cinema.md +0 -145
- package/.agent/skills/motion-styles/prompts/recipe-steps.md +0 -145
- package/.agent/skills/motion-styles/prompts/rubber-hose-reel.md +0 -145
- package/.agent/skills/motion-styles/prompts/shadow-puppets.md +0 -145
- package/.agent/skills/motion-styles/prompts/size-lineup.md +0 -149
- package/.agent/skills/motion-styles/prompts/software-walkthrough.md +0 -147
- package/.agent/skills/motion-styles/prompts/stack-split.md +0 -145
- package/.agent/skills/motion-styles/prompts/sticker-pop.md +0 -145
- package/.agent/skills/motion-styles/prompts/storybook.md +0 -145
- package/.agent/skills/motion-styles/prompts/swiss-grid.md +0 -145
- package/.agent/skills/motion-styles/prompts/synth-grid.md +0 -147
- package/.agent/skills/motion-styles/prompts/tactics-board.md +0 -145
- package/.agent/skills/motion-styles/prompts/terminal.md +0 -145
- package/.agent/skills/motion-styles/prompts/tier-list.md +0 -143
- package/.agent/skills/motion-styles/prompts/timeline-rail.md +0 -145
- package/.agent/skills/motion-styles/prompts/top-5-countdown.md +0 -145
- package/.agent/skills/motion-styles/prompts/versus-split.md +0 -145
- package/.agent/skills/motion-styles/prompts/vhs-camcorder.md +0 -145
- package/.agent/skills/motion-styles/prompts/visual-novel.md +0 -145
- package/.agent/skills/motion-styles/prompts/warm-canvas.md +0 -145
- package/.agent/skills/motion-styles/prompts/woodcut-tale.md +0 -145
- package/.agent/skills/motion-styles/prompts/zine-collage.md +0 -145
- package/.agent/skills/motion-styles/sources/bio-glow.html +0 -639
- package/.agent/skills/motion-styles/sources/blueprint.html +0 -617
- package/.agent/skills/motion-styles/sources/breaking-desk.html +0 -585
- package/.agent/skills/motion-styles/sources/callout-kit.html +0 -586
- package/.agent/skills/motion-styles/sources/chalk-talk.html +0 -597
- package/.agent/skills/motion-styles/sources/chat-story.html +0 -597
- package/.agent/skills/motion-styles/sources/clay-diorama.html +0 -681
- package/.agent/skills/motion-styles/sources/clean-tech.html +0 -554
- package/.agent/skills/motion-styles/sources/comic-panels.html +0 -608
- package/.agent/skills/motion-styles/sources/cosmic-scale.html +0 -628
- package/.agent/skills/motion-styles/sources/creator-motion.html +0 -805
- package/.agent/skills/motion-styles/sources/data-city.html +0 -613
- package/.agent/skills/motion-styles/sources/data-story.html +0 -539
- package/.agent/skills/motion-styles/sources/deep-layers.html +0 -624
- package/.agent/skills/motion-styles/sources/doc-cinema.html +0 -590
- package/.agent/skills/motion-styles/sources/evidence-board.html +0 -610
- package/.agent/skills/motion-styles/sources/exploded-view.html +0 -618
- package/.agent/skills/motion-styles/sources/glass-icons.html +0 -612
- package/.agent/skills/motion-styles/sources/glitch-pack.html +0 -583
- package/.agent/skills/motion-styles/sources/globe-arcs.html +0 -621
- package/.agent/skills/motion-styles/sources/headline-backdrop.html +0 -584
- package/.agent/skills/motion-styles/sources/iso-cutaway.html +0 -621
- package/.agent/skills/motion-styles/sources/isotype.html +0 -588
- package/.agent/skills/motion-styles/sources/karaoke-subs.html +0 -577
- package/.agent/skills/motion-styles/sources/low-poly-atlas.html +0 -621
- package/.agent/skills/motion-styles/sources/lower-thirds.html +0 -582
- package/.agent/skills/motion-styles/sources/marker-board.html +0 -598
- package/.agent/skills/motion-styles/sources/market-tape.html +0 -598
- package/.agent/skills/motion-styles/sources/math-canvas.html +0 -592
- package/.agent/skills/motion-styles/sources/myth-or-fact.html +0 -587
- package/.agent/skills/motion-styles/sources/neon-line.html +0 -597
- package/.agent/skills/motion-styles/sources/paper-craft.html +0 -612
- package/.agent/skills/motion-styles/sources/parchment-map.html +0 -624
- package/.agent/skills/motion-styles/sources/physics-toybox.html +0 -617
- package/.agent/skills/motion-styles/sources/pixel-quest.html +0 -609
- package/.agent/skills/motion-styles/sources/podcast-clip.html +0 -585
- package/.agent/skills/motion-styles/sources/pop-info.html +0 -677
- package/.agent/skills/motion-styles/sources/product-turntable.html +0 -611
- package/.agent/skills/motion-styles/sources/punch-captions.html +0 -583
- package/.agent/skills/motion-styles/sources/quiz-countdown.html +0 -593
- package/.agent/skills/motion-styles/sources/quote-cinema.html +0 -581
- package/.agent/skills/motion-styles/sources/recipe-steps.html +0 -599
- package/.agent/skills/motion-styles/sources/rubber-hose-reel.html +0 -627
- package/.agent/skills/motion-styles/sources/shadow-puppets.html +0 -609
- package/.agent/skills/motion-styles/sources/size-lineup.html +0 -620
- package/.agent/skills/motion-styles/sources/software-walkthrough.html +0 -651
- package/.agent/skills/motion-styles/sources/stack-split.html +0 -588
- package/.agent/skills/motion-styles/sources/sticker-pop.html +0 -587
- package/.agent/skills/motion-styles/sources/storybook.html +0 -600
- package/.agent/skills/motion-styles/sources/swiss-grid.html +0 -590
- package/.agent/skills/motion-styles/sources/synth-grid.html +0 -614
- package/.agent/skills/motion-styles/sources/tactics-board.html +0 -608
- package/.agent/skills/motion-styles/sources/terminal.html +0 -593
- package/.agent/skills/motion-styles/sources/tier-list.html +0 -600
- package/.agent/skills/motion-styles/sources/timeline-rail.html +0 -598
- package/.agent/skills/motion-styles/sources/top-5-countdown.html +0 -589
- package/.agent/skills/motion-styles/sources/versus-split.html +0 -592
- package/.agent/skills/motion-styles/sources/vhs-camcorder.html +0 -587
- package/.agent/skills/motion-styles/sources/visual-novel.html +0 -623
- package/.agent/skills/motion-styles/sources/warm-canvas.html +0 -742
- package/.agent/skills/motion-styles/sources/woodcut-tale.html +0 -598
- package/.agent/skills/motion-styles/sources/zine-collage.html +0 -594
- package/.agent/skills/reverse-engineer-anything/SKILL.md +0 -356
- package/.agent/skills/reverse-engineer-anything/references/android-applications.md +0 -36
- package/.agent/skills/reverse-engineer-anything/references/connection-and-recovery.md +0 -127
- package/.agent/skills/reverse-engineer-anything/references/evidence-workflows.md +0 -71
- package/.agent/skills/reverse-engineer-anything/references/javascript-applications.md +0 -218
- package/.agent/skills/reverse-engineer-anything/references/native-and-artifacts.md +0 -297
- package/.agent/skills/reverse-engineer-anything/references/runtime-observation.md +0 -78
- package/.agent/skills/video-pipeline/SKILL.md +0 -384
- package/.agent/skills/video-pipeline/assets/project/ANIMATION_GUIDE.md +0 -258
- package/.agent/skills/video-pipeline/assets/project/STORYBOARD.md +0 -77
- package/.agent/skills/video-pipeline/assets/project/index.html +0 -80
- package/.agent/skills/video-pipeline/assets/project/pipeline/briefings/diagram-author.md +0 -43
- package/.agent/skills/video-pipeline/assets/project/pipeline/briefings/orchestrator.md +0 -75
- package/.agent/skills/video-pipeline/assets/project/pipeline/briefings/scene-author.md +0 -63
- package/.agent/skills/video-pipeline/assets/project/reel/engine.css +0 -140
- package/.agent/skills/video-pipeline/assets/project/reel/engine.js +0 -2143
- package/.agent/skills/video-pipeline/assets/project/reel/virtual-clock.js +0 -168
- package/.agent/skills/video-pipeline/references/mcp-tools.md +0 -441
- package/.agent/skills/video-pipeline/references/motion-craft.md +0 -91
- package/.agent/skills/video-pipeline/references/providers.md +0 -174
- package/.agent/skills/video-pipeline/references/render-contract.md +0 -65
- package/plugins/aiinsight-shorts/styles/deep-dive/style-sheet.png +0 -0
- package/plugins/aiinsight-shorts/styles/inside-out/style-sheet.png +0 -0
- package/plugins/aiinsight-shorts/styles/night-file/style-sheet.png +0 -0
- package/plugins/aiinsight-shorts/styles/paper-atlas/style-sheet.png +0 -0
- package/plugins/aiinsight-shorts/styles/stick-story/style-sheet.png +0 -0
- package/plugins/aiinsight-shorts/styles/tall-tale/style-sheet.png +0 -0
- package/plugins/faceless-shorts/styles/deep-dive/style-sheet.png +0 -0
- package/plugins/faceless-shorts/styles/inside-out/style-sheet.png +0 -0
- package/plugins/faceless-shorts/styles/night-file/style-sheet.png +0 -0
- package/plugins/faceless-shorts/styles/paper-atlas/style-sheet.png +0 -0
- package/plugins/faceless-shorts/styles/stick-story/style-sheet.png +0 -0
- package/plugins/faceless-shorts/styles/tall-tale/style-sheet.png +0 -0
|
@@ -1,2779 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Integration tests for all browse commands
|
|
3
|
-
*
|
|
4
|
-
* Tests run against a local test server serving fixture HTML files.
|
|
5
|
-
* A real browse server is started and commands are sent via the CLI HTTP interface.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import * as os from 'os';
|
|
9
|
-
import { afterAll, beforeAll, describe, expect, test } from 'bun:test';
|
|
10
|
-
import { startTestServer } from './test-server';
|
|
11
|
-
import { BrowserManager } from '../src/browser-manager';
|
|
12
|
-
import { resolveServerScript } from '../src/cli';
|
|
13
|
-
import { handleReadCommand as _handleReadCommand, parseOutArgs, hasOutArg, resultToString } from '../src/read-commands';
|
|
14
|
-
import { handleWriteCommand as _handleWriteCommand } from '../src/write-commands';
|
|
15
|
-
import { handleMetaCommand } from '../src/meta-commands';
|
|
16
|
-
import { WRITE_COMMANDS, READ_COMMANDS, META_COMMANDS, PAGE_CONTENT_COMMANDS, wrapUntrustedContent } from '../src/commands';
|
|
17
|
-
import { consoleBuffer, networkBuffer, dialogBuffer, addConsoleEntry, addNetworkEntry, addDialogEntry, CircularBuffer } from '../src/buffers';
|
|
18
|
-
import * as fs from 'fs';
|
|
19
|
-
import { spawn } from 'child_process';
|
|
20
|
-
import { isProcessAlive } from '../src/error-handling';
|
|
21
|
-
import * as path from 'path';
|
|
22
|
-
import * as os from 'os';
|
|
23
|
-
|
|
24
|
-
// Temp files live under os.tmpdir(), never a hardcoded /tmp: macOS points
|
|
25
|
-
// tmpdir at a per-user private dir, and syscall-supervised sandboxes
|
|
26
|
-
// (Vercel) blanket-deny access(2) under /tmp for busy processes while
|
|
27
|
-
// honoring TMPDIR overrides. Hardcoded /tmp is a portability smell.
|
|
28
|
-
const tmpp = (name: string) => path.join(os.tmpdir(), name);
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
// Per-FILE Chromium profile: this file launches an in-process persistent
|
|
32
|
-
// context (BrowserManager.launch()), and sharing a profile dir with the
|
|
33
|
-
// long-lived browse daemon a sibling file may have spawned kills one side's
|
|
34
|
-
// Chromium (ProcessSingleton on user-data-dir). Scoped via hooks, never
|
|
35
|
-
// module scope (see test/gstack-home-module-scope.test.ts's rationale).
|
|
36
|
-
const ORIGINAL_CHROMIUM_PROFILE = process.env.CHROMIUM_PROFILE;
|
|
37
|
-
let CHROMIUM_PROFILE_DIR: string | undefined;
|
|
38
|
-
beforeAll(() => {
|
|
39
|
-
CHROMIUM_PROFILE_DIR = fs.mkdtempSync(path.join(os.tmpdir(), 'gstack-test-profile-'));
|
|
40
|
-
process.env.CHROMIUM_PROFILE = CHROMIUM_PROFILE_DIR;
|
|
41
|
-
});
|
|
42
|
-
afterAll(() => {
|
|
43
|
-
if (ORIGINAL_CHROMIUM_PROFILE === undefined) delete process.env.CHROMIUM_PROFILE;
|
|
44
|
-
else process.env.CHROMIUM_PROFILE = ORIGINAL_CHROMIUM_PROFILE;
|
|
45
|
-
if (CHROMIUM_PROFILE_DIR) { try { fs.rmSync(CHROMIUM_PROFILE_DIR, { recursive: true, force: true }); } catch {} }
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
// Thin wrappers that bridge old test calls (bm as 3rd arg) to new signatures (session + bm)
|
|
50
|
-
const handleReadCommand = (cmd: string, args: string[], b: BrowserManager) =>
|
|
51
|
-
_handleReadCommand(cmd, args, b.getActiveSession(), b);
|
|
52
|
-
const handleWriteCommand = (cmd: string, args: string[], b: BrowserManager) =>
|
|
53
|
-
_handleWriteCommand(cmd, args, b.getActiveSession(), b);
|
|
54
|
-
|
|
55
|
-
// Chain routes every subcommand through the server's executeCommand pipeline in
|
|
56
|
-
// production (the direct-dispatch fallback was deleted — it skipped the security
|
|
57
|
-
// gates). Tests mirror the pipeline minimally: real handlers + trust-wrapping,
|
|
58
|
-
// server-shaped {status, result} envelope.
|
|
59
|
-
function makeChainExecute(b: BrowserManager) {
|
|
60
|
-
return async (body: { command: string; args?: string[] }) => {
|
|
61
|
-
const name = body.command;
|
|
62
|
-
const args = body.args ?? [];
|
|
63
|
-
try {
|
|
64
|
-
let result: string;
|
|
65
|
-
if (WRITE_COMMANDS.has(name)) {
|
|
66
|
-
result = await _handleWriteCommand(name, args, b.getActiveSession(), b);
|
|
67
|
-
} else if (READ_COMMANDS.has(name)) {
|
|
68
|
-
result = await _handleReadCommand(name, args, b.getActiveSession(), b);
|
|
69
|
-
if (PAGE_CONTENT_COMMANDS.has(name)) {
|
|
70
|
-
result = wrapUntrustedContent(result, b.getCurrentUrl());
|
|
71
|
-
}
|
|
72
|
-
} else if (META_COMMANDS.has(name)) {
|
|
73
|
-
result = await handleMetaCommand(name, args, b, async () => {});
|
|
74
|
-
} else {
|
|
75
|
-
return { status: 404, result: JSON.stringify({ error: `Unknown command: ${name}` }) };
|
|
76
|
-
}
|
|
77
|
-
return { status: 200, result };
|
|
78
|
-
} catch (err: any) {
|
|
79
|
-
return { status: 500, result: JSON.stringify({ error: err.message }) };
|
|
80
|
-
}
|
|
81
|
-
};
|
|
82
|
-
}
|
|
83
|
-
const chainMeta = (b: BrowserManager, args: string[]) =>
|
|
84
|
-
handleMetaCommand('chain', args, b, async () => {}, null, { executeCommand: makeChainExecute(b) });
|
|
85
|
-
|
|
86
|
-
// ─── Pure arg-parser + result-conversion unit tests (no browser) ───
|
|
87
|
-
describe('parseOutArgs / hasOutArg', () => {
|
|
88
|
-
test('--out <path> splits the flag from the positional', () => {
|
|
89
|
-
expect(parseOutArgs(['expr', '--out', '/tmp/x'])).toEqual({ outPath: '/tmp/x', raw: false, rest: ['expr'] });
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
test('--out=<path> form is equivalent', () => {
|
|
93
|
-
expect(parseOutArgs(['expr', '--out=/tmp/x'])).toEqual({ outPath: '/tmp/x', raw: false, rest: ['expr'] });
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
test('flag ordering does not matter', () => {
|
|
97
|
-
expect(parseOutArgs(['--out', '/tmp/x', 'expr'])).toEqual({ outPath: '/tmp/x', raw: false, rest: ['expr'] });
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
test('--raw and --raw=true|false', () => {
|
|
101
|
-
expect(parseOutArgs(['e', '--out', '/tmp/x', '--raw']).raw).toBe(true);
|
|
102
|
-
expect(parseOutArgs(['e', '--out', '/tmp/x', '--raw=true']).raw).toBe(true);
|
|
103
|
-
expect(parseOutArgs(['e', '--out', '/tmp/x', '--raw=false']).raw).toBe(false);
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
test('repeated --out throws', () => {
|
|
107
|
-
expect(() => parseOutArgs(['e', '--out', '/a', '--out', '/b'])).toThrow(/more than once/);
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
test('--out with a missing value throws', () => {
|
|
111
|
-
expect(() => parseOutArgs(['e', '--out'])).toThrow(/requires a file path/);
|
|
112
|
-
expect(() => parseOutArgs(['e', '--out', '--raw'])).toThrow(/requires a file path/);
|
|
113
|
-
expect(() => parseOutArgs(['e', '--out='])).toThrow(/requires a file path/);
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
test('bad --raw value throws', () => {
|
|
117
|
-
expect(() => parseOutArgs(['e', '--out', '/a', '--raw=maybe'])).toThrow(/--raw must be true or false/);
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
test('hasOutArg matches --out and --out= exactly, not lookalikes', () => {
|
|
121
|
-
expect(hasOutArg(['a', '--out', 'b'])).toBe(true);
|
|
122
|
-
expect(hasOutArg(['a', '--out=b'])).toBe(true);
|
|
123
|
-
expect(hasOutArg(['a'])).toBe(false);
|
|
124
|
-
expect(hasOutArg(['a', '--output', 'b'])).toBe(false);
|
|
125
|
-
expect(hasOutArg(['a', '--outx'])).toBe(false);
|
|
126
|
-
});
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
describe('resultToString — byte-for-byte with pre-refactor behavior', () => {
|
|
130
|
-
test('null becomes "null" (typeof null === object → JSON.stringify)', () => {
|
|
131
|
-
expect(resultToString(null)).toBe('null');
|
|
132
|
-
});
|
|
133
|
-
test('undefined becomes empty string', () => {
|
|
134
|
-
expect(resultToString(undefined)).toBe('');
|
|
135
|
-
});
|
|
136
|
-
test('objects are pretty-printed JSON', () => {
|
|
137
|
-
expect(resultToString({ a: 1 })).toBe(JSON.stringify({ a: 1 }, null, 2));
|
|
138
|
-
});
|
|
139
|
-
test('primitives use String()', () => {
|
|
140
|
-
expect(resultToString(42)).toBe('42');
|
|
141
|
-
expect(resultToString(true)).toBe('true');
|
|
142
|
-
});
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
let testServer: ReturnType<typeof startTestServer>;
|
|
146
|
-
let bm: BrowserManager;
|
|
147
|
-
let baseUrl: string;
|
|
148
|
-
|
|
149
|
-
beforeAll(async () => {
|
|
150
|
-
testServer = startTestServer(0);
|
|
151
|
-
baseUrl = testServer.url;
|
|
152
|
-
|
|
153
|
-
bm = new BrowserManager();
|
|
154
|
-
await bm.launch();
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
afterAll(async () => {
|
|
158
|
-
try { testServer.server.stop(true); } catch {} // force-close keep-alives — a lingering Chromium connection otherwise blocks stop() forever
|
|
159
|
-
// Close only this file's own browser — never process.exit(): bun test runs
|
|
160
|
-
// all files in one process, so a delayed exit kills the whole suite
|
|
161
|
-
// (see test/no-suicide-exit.test.ts). close() can hang when the browser
|
|
162
|
-
// already died, and its internal 5s timeout ties bun's 5s hook timeout —
|
|
163
|
-
// so race it at 3s and abandon; the child is reaped at process exit.
|
|
164
|
-
try { await Promise.race([bm?.close(), new Promise((resolve) => setTimeout(resolve, 3000))]); } catch {}
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
// ─── Navigation ─────────────────────────────────────────────────
|
|
168
|
-
|
|
169
|
-
describe('Navigation', () => {
|
|
170
|
-
test('goto navigates to URL', async () => {
|
|
171
|
-
const result = await handleWriteCommand('goto', [baseUrl + '/basic.html'], bm);
|
|
172
|
-
expect(result).toContain('Navigated to');
|
|
173
|
-
expect(result).toContain('200');
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
test('url returns current URL', async () => {
|
|
177
|
-
const result = await handleMetaCommand('url', [], bm, async () => {});
|
|
178
|
-
expect(result).toContain('/basic.html');
|
|
179
|
-
});
|
|
180
|
-
|
|
181
|
-
test('back goes back', async () => {
|
|
182
|
-
await handleWriteCommand('goto', [baseUrl + '/forms.html'], bm);
|
|
183
|
-
const result = await handleWriteCommand('back', [], bm);
|
|
184
|
-
expect(result).toContain('Back');
|
|
185
|
-
});
|
|
186
|
-
|
|
187
|
-
test('forward goes forward', async () => {
|
|
188
|
-
const result = await handleWriteCommand('forward', [], bm);
|
|
189
|
-
expect(result).toContain('Forward');
|
|
190
|
-
});
|
|
191
|
-
|
|
192
|
-
test('reload reloads page', async () => {
|
|
193
|
-
const result = await handleWriteCommand('reload', [], bm);
|
|
194
|
-
expect(result).toContain('Reloaded');
|
|
195
|
-
});
|
|
196
|
-
});
|
|
197
|
-
|
|
198
|
-
// ─── Content Extraction ─────────────────────────────────────────
|
|
199
|
-
|
|
200
|
-
describe('Content extraction', () => {
|
|
201
|
-
beforeAll(async () => {
|
|
202
|
-
await handleWriteCommand('goto', [baseUrl + '/basic.html'], bm);
|
|
203
|
-
});
|
|
204
|
-
|
|
205
|
-
test('text returns cleaned page text', async () => {
|
|
206
|
-
const result = await handleReadCommand('text', [], bm);
|
|
207
|
-
expect(result).toContain('Hello World');
|
|
208
|
-
expect(result).toContain('Item one');
|
|
209
|
-
expect(result).not.toContain('<h1>');
|
|
210
|
-
});
|
|
211
|
-
|
|
212
|
-
test('html returns full page HTML', async () => {
|
|
213
|
-
const result = await handleReadCommand('html', [], bm);
|
|
214
|
-
expect(result).toContain('<!DOCTYPE html>');
|
|
215
|
-
expect(result).toContain('<h1 id="title">Hello World</h1>');
|
|
216
|
-
});
|
|
217
|
-
|
|
218
|
-
test('html with selector returns element innerHTML', async () => {
|
|
219
|
-
const result = await handleReadCommand('html', ['#content'], bm);
|
|
220
|
-
expect(result).toContain('Some body text here.');
|
|
221
|
-
expect(result).toContain('<li>Item one</li>');
|
|
222
|
-
});
|
|
223
|
-
|
|
224
|
-
test('links returns all links', async () => {
|
|
225
|
-
const result = await handleReadCommand('links', [], bm);
|
|
226
|
-
expect(result).toContain('Page 1');
|
|
227
|
-
expect(result).toContain('Page 2');
|
|
228
|
-
expect(result).toContain('External');
|
|
229
|
-
expect(result).toContain('→');
|
|
230
|
-
});
|
|
231
|
-
|
|
232
|
-
test('forms discovers form fields', async () => {
|
|
233
|
-
await handleWriteCommand('goto', [baseUrl + '/forms.html'], bm);
|
|
234
|
-
const result = await handleReadCommand('forms', [], bm);
|
|
235
|
-
const forms = JSON.parse(result);
|
|
236
|
-
expect(forms.length).toBe(2);
|
|
237
|
-
expect(forms[0].id).toBe('login-form');
|
|
238
|
-
expect(forms[0].method).toBe('post');
|
|
239
|
-
expect(forms[0].fields.length).toBeGreaterThanOrEqual(2);
|
|
240
|
-
expect(forms[1].id).toBe('profile-form');
|
|
241
|
-
|
|
242
|
-
// Check field discovery
|
|
243
|
-
const emailField = forms[0].fields.find((f: any) => f.name === 'email');
|
|
244
|
-
expect(emailField).toBeDefined();
|
|
245
|
-
expect(emailField.type).toBe('email');
|
|
246
|
-
expect(emailField.required).toBe(true);
|
|
247
|
-
});
|
|
248
|
-
|
|
249
|
-
test('accessibility returns ARIA tree', async () => {
|
|
250
|
-
await handleWriteCommand('goto', [baseUrl + '/basic.html'], bm);
|
|
251
|
-
const result = await handleReadCommand('accessibility', [], bm);
|
|
252
|
-
expect(result).toContain('Hello World');
|
|
253
|
-
});
|
|
254
|
-
});
|
|
255
|
-
|
|
256
|
-
// ─── JavaScript / CSS / Attrs ───────────────────────────────────
|
|
257
|
-
|
|
258
|
-
describe('Inspection', () => {
|
|
259
|
-
beforeAll(async () => {
|
|
260
|
-
await handleWriteCommand('goto', [baseUrl + '/basic.html'], bm);
|
|
261
|
-
});
|
|
262
|
-
|
|
263
|
-
test('js evaluates expression', async () => {
|
|
264
|
-
const result = await handleReadCommand('js', ['document.title'], bm);
|
|
265
|
-
expect(result).toBe('Test Page - Basic');
|
|
266
|
-
});
|
|
267
|
-
|
|
268
|
-
test('js returns objects as JSON', async () => {
|
|
269
|
-
const result = await handleReadCommand('js', ['({a: 1, b: 2})'], bm);
|
|
270
|
-
const obj = JSON.parse(result);
|
|
271
|
-
expect(obj.a).toBe(1);
|
|
272
|
-
expect(obj.b).toBe(2);
|
|
273
|
-
});
|
|
274
|
-
|
|
275
|
-
test('js supports await expressions', async () => {
|
|
276
|
-
const result = await handleReadCommand('js', ['await Promise.resolve(42)'], bm);
|
|
277
|
-
expect(result).toBe('42');
|
|
278
|
-
});
|
|
279
|
-
|
|
280
|
-
test('js does not false-positive on await substring', async () => {
|
|
281
|
-
const result = await handleReadCommand('js', ['(() => { const awaitable = 5; return awaitable })()'], bm);
|
|
282
|
-
expect(result).toBe('5');
|
|
283
|
-
});
|
|
284
|
-
|
|
285
|
-
test('eval supports await in single-line file', async () => {
|
|
286
|
-
const tmp = tmpp('eval-await-test.js');
|
|
287
|
-
fs.writeFileSync(tmp, 'await Promise.resolve("hello from eval")');
|
|
288
|
-
try {
|
|
289
|
-
const result = await handleReadCommand('eval', [tmp], bm);
|
|
290
|
-
expect(result).toBe('hello from eval');
|
|
291
|
-
} finally {
|
|
292
|
-
fs.unlinkSync(tmp);
|
|
293
|
-
}
|
|
294
|
-
});
|
|
295
|
-
|
|
296
|
-
test('eval does not wrap when await is only in a comment', async () => {
|
|
297
|
-
const tmp = tmpp('eval-comment-test.js');
|
|
298
|
-
fs.writeFileSync(tmp, '// no need to await this\ndocument.title');
|
|
299
|
-
try {
|
|
300
|
-
const result = await handleReadCommand('eval', [tmp], bm);
|
|
301
|
-
expect(result).toBe('Test Page - Basic');
|
|
302
|
-
} finally {
|
|
303
|
-
fs.unlinkSync(tmp);
|
|
304
|
-
}
|
|
305
|
-
});
|
|
306
|
-
|
|
307
|
-
test('eval multi-line with await and explicit return', async () => {
|
|
308
|
-
const tmp = tmpp('eval-multiline-await.js');
|
|
309
|
-
fs.writeFileSync(tmp, 'const data = await Promise.resolve("multi");\nreturn data;');
|
|
310
|
-
try {
|
|
311
|
-
const result = await handleReadCommand('eval', [tmp], bm);
|
|
312
|
-
expect(result).toBe('multi');
|
|
313
|
-
} finally {
|
|
314
|
-
fs.unlinkSync(tmp);
|
|
315
|
-
}
|
|
316
|
-
});
|
|
317
|
-
|
|
318
|
-
test('eval multi-line with await but no return gives empty string', async () => {
|
|
319
|
-
const tmp = tmpp('eval-multiline-no-return.js');
|
|
320
|
-
fs.writeFileSync(tmp, 'const data = await Promise.resolve("lost");\ndata;');
|
|
321
|
-
try {
|
|
322
|
-
const result = await handleReadCommand('eval', [tmp], bm);
|
|
323
|
-
expect(result).toBe('');
|
|
324
|
-
} finally {
|
|
325
|
-
fs.unlinkSync(tmp);
|
|
326
|
-
}
|
|
327
|
-
});
|
|
328
|
-
|
|
329
|
-
test('js handles multi-line with await', async () => {
|
|
330
|
-
const code = 'const x = await Promise.resolve(42);\nreturn x;';
|
|
331
|
-
const result = await handleReadCommand('js', [code], bm);
|
|
332
|
-
expect(result).toBe('42');
|
|
333
|
-
});
|
|
334
|
-
|
|
335
|
-
test('js handles await with semicolons', async () => {
|
|
336
|
-
const result = await handleReadCommand('js', ['const x = await Promise.resolve(5); return x + 1;'], bm);
|
|
337
|
-
expect(result).toBe('6');
|
|
338
|
-
});
|
|
339
|
-
|
|
340
|
-
test('js handles await with statement keywords', async () => {
|
|
341
|
-
const result = await handleReadCommand('js', ['const res = await Promise.resolve("ok"); return res;'], bm);
|
|
342
|
-
expect(result).toBe('ok');
|
|
343
|
-
});
|
|
344
|
-
|
|
345
|
-
test('js still works for simple expressions', async () => {
|
|
346
|
-
const result = await handleReadCommand('js', ['1 + 2'], bm);
|
|
347
|
-
expect(result).toBe('3');
|
|
348
|
-
});
|
|
349
|
-
|
|
350
|
-
// ─── js/eval --out (render-to-file) ───────────────────────────
|
|
351
|
-
|
|
352
|
-
test('js (no --out) returns a multi-MB string without truncation', async () => {
|
|
353
|
-
// Handler-level guarantee: the result is not sliced/capped before return.
|
|
354
|
-
// (Full HTTP egress path is exercised elsewhere; this pins the handler.)
|
|
355
|
-
const result = await handleReadCommand('js', ["'x'.repeat(3 * 1024 * 1024)"], bm);
|
|
356
|
-
expect(result.length).toBe(3 * 1024 * 1024);
|
|
357
|
-
});
|
|
358
|
-
|
|
359
|
-
test('js --out writes the result to disk and returns a short status, not the payload', async () => {
|
|
360
|
-
const out = tmpp(`browse-out-large-${Date.now()}.txt`);
|
|
361
|
-
try {
|
|
362
|
-
const result = await handleReadCommand('js', ["'y'.repeat(2 * 1024 * 1024)", '--out', out], bm);
|
|
363
|
-
expect(result).toContain('JS result written:');
|
|
364
|
-
expect(result).toContain(out);
|
|
365
|
-
expect(result).toContain(`(${2 * 1024 * 1024} bytes)`);
|
|
366
|
-
expect(result.length).toBeLessThan(200); // status, not the 2MB payload
|
|
367
|
-
expect(fs.statSync(out).size).toBe(2 * 1024 * 1024);
|
|
368
|
-
} finally {
|
|
369
|
-
fs.rmSync(out, { force: true });
|
|
370
|
-
}
|
|
371
|
-
});
|
|
372
|
-
|
|
373
|
-
test('js --out decodes a base64 PNG data URL to real bytes', async () => {
|
|
374
|
-
// 1x1 transparent PNG.
|
|
375
|
-
const b64 = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==';
|
|
376
|
-
const out = tmpp(`browse-out-png-${Date.now()}.png`);
|
|
377
|
-
try {
|
|
378
|
-
const result = await handleReadCommand('js', [`'data:image/png;base64,' + '${b64}'`, '--out', out], bm);
|
|
379
|
-
const buf = fs.readFileSync(out);
|
|
380
|
-
// PNG magic bytes: 89 50 4E 47
|
|
381
|
-
expect([buf[0], buf[1], buf[2], buf[3]]).toEqual([0x89, 0x50, 0x4e, 0x47]);
|
|
382
|
-
const expectedLen = Buffer.from(b64, 'base64').length;
|
|
383
|
-
expect(buf.length).toBe(expectedLen);
|
|
384
|
-
expect(result).toContain(`(${expectedLen} bytes)`);
|
|
385
|
-
} finally {
|
|
386
|
-
fs.rmSync(out, { force: true });
|
|
387
|
-
}
|
|
388
|
-
});
|
|
389
|
-
|
|
390
|
-
test('js --out --raw writes the literal data-URL string (no decode)', async () => {
|
|
391
|
-
const dataUrl = 'data:text/plain;base64,aGVsbG8=';
|
|
392
|
-
const out = tmpp(`browse-out-raw-${Date.now()}.txt`);
|
|
393
|
-
try {
|
|
394
|
-
await handleReadCommand('js', [`'${dataUrl}'`, '--out', out, '--raw'], bm);
|
|
395
|
-
expect(fs.readFileSync(out, 'utf-8')).toBe(dataUrl);
|
|
396
|
-
} finally {
|
|
397
|
-
fs.rmSync(out, { force: true });
|
|
398
|
-
}
|
|
399
|
-
});
|
|
400
|
-
|
|
401
|
-
test('js --out throws on a malformed base64 data URL instead of writing corrupt bytes', async () => {
|
|
402
|
-
const out = tmpp(`browse-out-bad-${Date.now()}.png`);
|
|
403
|
-
try {
|
|
404
|
-
await expect(
|
|
405
|
-
handleReadCommand('js', ["'data:image/png;base64,!!!not-base64!!!'", '--out', out], bm)
|
|
406
|
-
).rejects.toThrow(/malformed base64/);
|
|
407
|
-
expect(fs.existsSync(out)).toBe(false);
|
|
408
|
-
} finally {
|
|
409
|
-
fs.rmSync(out, { force: true });
|
|
410
|
-
}
|
|
411
|
-
});
|
|
412
|
-
|
|
413
|
-
test('js --out rejects a path outside the safe directories', async () => {
|
|
414
|
-
await expect(
|
|
415
|
-
handleReadCommand('js', ['1 + 1', '--out', '/etc/browse-should-not-write.txt'], bm)
|
|
416
|
-
).rejects.toThrow();
|
|
417
|
-
});
|
|
418
|
-
|
|
419
|
-
test('js --out creates a missing parent directory', async () => {
|
|
420
|
-
// validateOutputPath resolves the parent's realpath, so it permits one level
|
|
421
|
-
// of missing dir under a safe root (/tmp). mkdir then materializes it.
|
|
422
|
-
const root = tmpp(`browse-out-nested-${Date.now()}`);
|
|
423
|
-
const out = `${root}/result.txt`;
|
|
424
|
-
try {
|
|
425
|
-
await handleReadCommand('js', ["'nested'", '--out', out], bm);
|
|
426
|
-
expect(fs.readFileSync(out, 'utf-8')).toBe('nested');
|
|
427
|
-
} finally {
|
|
428
|
-
fs.rmSync(root, { recursive: true, force: true });
|
|
429
|
-
}
|
|
430
|
-
});
|
|
431
|
-
|
|
432
|
-
test('eval --out writes the file result to disk (parity with js)', async () => {
|
|
433
|
-
const script = tmpp(`browse-eval-out-src-${Date.now()}.js`);
|
|
434
|
-
const out = tmpp(`browse-eval-out-${Date.now()}.txt`);
|
|
435
|
-
fs.writeFileSync(script, "'from eval'");
|
|
436
|
-
try {
|
|
437
|
-
const result = await handleReadCommand('eval', [script, '--out', out], bm);
|
|
438
|
-
expect(result).toContain('Eval result written:');
|
|
439
|
-
expect(fs.readFileSync(out, 'utf-8')).toBe('from eval');
|
|
440
|
-
} finally {
|
|
441
|
-
fs.rmSync(script, { force: true });
|
|
442
|
-
fs.rmSync(out, { force: true });
|
|
443
|
-
}
|
|
444
|
-
});
|
|
445
|
-
|
|
446
|
-
test('css returns computed property', async () => {
|
|
447
|
-
const result = await handleReadCommand('css', ['h1', 'color'], bm);
|
|
448
|
-
// Navy color
|
|
449
|
-
expect(result).toContain('0, 0, 128');
|
|
450
|
-
});
|
|
451
|
-
|
|
452
|
-
test('css returns font-family', async () => {
|
|
453
|
-
const result = await handleReadCommand('css', ['body', 'font-family'], bm);
|
|
454
|
-
expect(result).toContain('Helvetica');
|
|
455
|
-
});
|
|
456
|
-
|
|
457
|
-
test('attrs returns element attributes', async () => {
|
|
458
|
-
const result = await handleReadCommand('attrs', ['#content'], bm);
|
|
459
|
-
const attrs = JSON.parse(result);
|
|
460
|
-
expect(attrs.id).toBe('content');
|
|
461
|
-
expect(attrs['data-testid']).toBe('main-content');
|
|
462
|
-
expect(attrs['data-version']).toBe('1.0');
|
|
463
|
-
});
|
|
464
|
-
});
|
|
465
|
-
|
|
466
|
-
// ─── Interaction ────────────────────────────────────────────────
|
|
467
|
-
|
|
468
|
-
describe('Interaction', () => {
|
|
469
|
-
test('fill + click works on form', async () => {
|
|
470
|
-
await handleWriteCommand('goto', [baseUrl + '/forms.html'], bm);
|
|
471
|
-
|
|
472
|
-
let result = await handleWriteCommand('fill', ['#email', 'test@example.com'], bm);
|
|
473
|
-
expect(result).toContain('Filled');
|
|
474
|
-
|
|
475
|
-
result = await handleWriteCommand('fill', ['#password', 'secret123'], bm);
|
|
476
|
-
expect(result).toContain('Filled');
|
|
477
|
-
|
|
478
|
-
// Verify values were set
|
|
479
|
-
const emailVal = await handleReadCommand('js', ['document.querySelector("#email").value'], bm);
|
|
480
|
-
expect(emailVal).toBe('test@example.com');
|
|
481
|
-
|
|
482
|
-
result = await handleWriteCommand('click', ['#login-btn'], bm);
|
|
483
|
-
expect(result).toContain('Clicked');
|
|
484
|
-
});
|
|
485
|
-
|
|
486
|
-
test('select works on dropdown', async () => {
|
|
487
|
-
await handleWriteCommand('goto', [baseUrl + '/forms.html'], bm);
|
|
488
|
-
const result = await handleWriteCommand('select', ['#role', 'admin'], bm);
|
|
489
|
-
expect(result).toContain('Selected');
|
|
490
|
-
|
|
491
|
-
const val = await handleReadCommand('js', ['document.querySelector("#role").value'], bm);
|
|
492
|
-
expect(val).toBe('admin');
|
|
493
|
-
});
|
|
494
|
-
|
|
495
|
-
test('click on option ref auto-routes to selectOption', async () => {
|
|
496
|
-
await handleWriteCommand('goto', [baseUrl + '/forms.html'], bm);
|
|
497
|
-
// Reset select to default
|
|
498
|
-
await handleReadCommand('js', ['document.querySelector("#role").value = ""'], bm);
|
|
499
|
-
const snap = await handleMetaCommand('snapshot', [], bm, async () => {});
|
|
500
|
-
// Find an option ref (e.g., "Admin" option)
|
|
501
|
-
const optionLine = snap.split('\n').find((l: string) => l.includes('[option]') && l.includes('"Admin"'));
|
|
502
|
-
expect(optionLine).toBeDefined();
|
|
503
|
-
const refMatch = optionLine!.match(/@(e\d+)/);
|
|
504
|
-
expect(refMatch).toBeDefined();
|
|
505
|
-
const ref = `@${refMatch![1]}`;
|
|
506
|
-
const result = await handleWriteCommand('click', [ref], bm);
|
|
507
|
-
expect(result).toContain('auto-routed');
|
|
508
|
-
expect(result).toContain('Selected');
|
|
509
|
-
// Verify the select value actually changed
|
|
510
|
-
const val = await handleReadCommand('js', ['document.querySelector("#role").value'], bm);
|
|
511
|
-
expect(val).toBe('admin');
|
|
512
|
-
});
|
|
513
|
-
|
|
514
|
-
test('click CSS selector on option gives helpful error', async () => {
|
|
515
|
-
await handleWriteCommand('goto', [baseUrl + '/forms.html'], bm);
|
|
516
|
-
try {
|
|
517
|
-
await handleWriteCommand('click', ['option[value="admin"]'], bm);
|
|
518
|
-
expect(true).toBe(false); // Should not reach here
|
|
519
|
-
} catch (err: any) {
|
|
520
|
-
expect(err.message).toContain('select');
|
|
521
|
-
expect(err.message).toContain('option');
|
|
522
|
-
}
|
|
523
|
-
}, 15000);
|
|
524
|
-
|
|
525
|
-
test('hover works', async () => {
|
|
526
|
-
const result = await handleWriteCommand('hover', ['h1'], bm);
|
|
527
|
-
expect(result).toContain('Hovered');
|
|
528
|
-
});
|
|
529
|
-
|
|
530
|
-
test('wait finds existing element', async () => {
|
|
531
|
-
await handleWriteCommand('goto', [baseUrl + '/basic.html'], bm);
|
|
532
|
-
const result = await handleWriteCommand('wait', ['#title'], bm);
|
|
533
|
-
expect(result).toContain('appeared');
|
|
534
|
-
});
|
|
535
|
-
|
|
536
|
-
test('scroll works', async () => {
|
|
537
|
-
const result = await handleWriteCommand('scroll', ['footer'], bm);
|
|
538
|
-
expect(result).toContain('Scrolled');
|
|
539
|
-
});
|
|
540
|
-
|
|
541
|
-
test('viewport changes size', async () => {
|
|
542
|
-
const result = await handleWriteCommand('viewport', ['375x812'], bm);
|
|
543
|
-
expect(result).toContain('Viewport set');
|
|
544
|
-
|
|
545
|
-
const size = await handleReadCommand('js', ['`${window.innerWidth}x${window.innerHeight}`'], bm);
|
|
546
|
-
expect(size).toBe('375x812');
|
|
547
|
-
|
|
548
|
-
// Reset
|
|
549
|
-
await handleWriteCommand('viewport', ['1280x720'], bm);
|
|
550
|
-
});
|
|
551
|
-
|
|
552
|
-
test('type and press work', async () => {
|
|
553
|
-
await handleWriteCommand('goto', [baseUrl + '/forms.html'], bm);
|
|
554
|
-
await handleWriteCommand('click', ['#name'], bm);
|
|
555
|
-
|
|
556
|
-
const result = await handleWriteCommand('type', ['John Doe'], bm);
|
|
557
|
-
expect(result).toContain('Typed');
|
|
558
|
-
|
|
559
|
-
const val = await handleReadCommand('js', ['document.querySelector("#name").value'], bm);
|
|
560
|
-
expect(val).toBe('John Doe');
|
|
561
|
-
});
|
|
562
|
-
|
|
563
|
-
test('type --selector types into that element, not the focused one (#2936)', async () => {
|
|
564
|
-
await handleWriteCommand('goto', [baseUrl + '/forms.html'], bm);
|
|
565
|
-
await handleWriteCommand('click', ['#email'], bm);
|
|
566
|
-
const result = await handleWriteCommand('type', ['--selector', '#name', 'Jane', 'Roe'], bm);
|
|
567
|
-
expect(result).toBe('Typed 8 characters into #name');
|
|
568
|
-
expect(await handleReadCommand('js', ['document.querySelector("#name").value'], bm)).toBe('Jane Roe');
|
|
569
|
-
expect(await handleReadCommand('js', ['document.querySelector("#email").value'], bm)).toBe('');
|
|
570
|
-
});
|
|
571
|
-
|
|
572
|
-
test('bare type keeps typing selector-looking text literally, with a hint and no refusal', async () => {
|
|
573
|
-
await handleWriteCommand('goto', [baseUrl + '/forms.html'], bm);
|
|
574
|
-
await handleWriteCommand('click', ['#bio'], bm);
|
|
575
|
-
const result = await handleWriteCommand('type', ['#name', 'secret-value'], bm);
|
|
576
|
-
expect(result).toContain('Typed 18 characters into the focused element');
|
|
577
|
-
expect(result).toContain("hint: \"#name\" looks like a CSS selector. To type into that element: browse type --selector '#name' <text>");
|
|
578
|
-
expect(result).not.toContain('secret-value');
|
|
579
|
-
expect(await handleReadCommand('js', ['document.querySelector("#bio").value'], bm)).toBe('#name secret-value');
|
|
580
|
-
expect(await handleReadCommand('js', ['document.querySelector("#name").value'], bm)).toBe('');
|
|
581
|
-
});
|
|
582
|
-
|
|
583
|
-
test('prettyscreenshot --hide keeps a trailing image path as the output path (#1419)', async () => {
|
|
584
|
-
await handleWriteCommand('goto', [baseUrl + '/basic.html'], bm);
|
|
585
|
-
const out = path.join(os.tmpdir(), `pretty-hide-${process.pid}.png`);
|
|
586
|
-
fs.rmSync(out, { force: true });
|
|
587
|
-
try {
|
|
588
|
-
const result = await handleWriteCommand('prettyscreenshot', ['--hide', '#title', '#content', out], bm);
|
|
589
|
-
expect(result).toContain(out);
|
|
590
|
-
expect(fs.existsSync(out)).toBe(true);
|
|
591
|
-
expect(await handleReadCommand('js', ['getComputedStyle(document.querySelector("#content")).display'], bm)).toBe('none');
|
|
592
|
-
} finally {
|
|
593
|
-
fs.rmSync(out, { force: true });
|
|
594
|
-
}
|
|
595
|
-
});
|
|
596
|
-
|
|
597
|
-
test('type -- types text that starts with -- literally; plain text gets no hint', async () => {
|
|
598
|
-
await handleWriteCommand('goto', [baseUrl + '/forms.html'], bm);
|
|
599
|
-
await handleWriteCommand('click', ['#bio'], bm);
|
|
600
|
-
expect(await handleWriteCommand('type', ['--', '--selector', 'x'], bm)).toBe('Typed 12 characters');
|
|
601
|
-
expect(await handleReadCommand('js', ['document.querySelector("#bio").value'], bm)).toBe('--selector x');
|
|
602
|
-
await expect(handleWriteCommand('type', ['--selector'], bm)).rejects.toThrow('Usage: browse type [--selector <sel>] [--] <text>');
|
|
603
|
-
await expect(handleWriteCommand('type', ['--selector', '#name'], bm)).rejects.toThrow('Usage');
|
|
604
|
-
});
|
|
605
|
-
});
|
|
606
|
-
|
|
607
|
-
// ─── SPA / Console / Network ───────────────────────────────────
|
|
608
|
-
|
|
609
|
-
describe('SPA and buffers', () => {
|
|
610
|
-
test('wait handles delayed rendering', async () => {
|
|
611
|
-
await handleWriteCommand('goto', [baseUrl + '/spa.html'], bm);
|
|
612
|
-
const result = await handleWriteCommand('wait', ['.loaded'], bm);
|
|
613
|
-
expect(result).toContain('appeared');
|
|
614
|
-
|
|
615
|
-
const text = await handleReadCommand('text', [], bm);
|
|
616
|
-
expect(text).toContain('SPA Content Loaded');
|
|
617
|
-
});
|
|
618
|
-
|
|
619
|
-
test('console captures messages', async () => {
|
|
620
|
-
const result = await handleReadCommand('console', [], bm);
|
|
621
|
-
expect(result).toContain('[SPA] Starting render');
|
|
622
|
-
expect(result).toContain('[SPA] Render complete');
|
|
623
|
-
});
|
|
624
|
-
|
|
625
|
-
test('console --clear clears buffer', async () => {
|
|
626
|
-
const result = await handleReadCommand('console', ['--clear'], bm);
|
|
627
|
-
expect(result).toContain('cleared');
|
|
628
|
-
|
|
629
|
-
const after = await handleReadCommand('console', [], bm);
|
|
630
|
-
expect(after).toContain('no console messages');
|
|
631
|
-
});
|
|
632
|
-
|
|
633
|
-
test('network captures requests', async () => {
|
|
634
|
-
const result = await handleReadCommand('network', [], bm);
|
|
635
|
-
expect(result).toContain('GET');
|
|
636
|
-
expect(result).toContain('/spa.html');
|
|
637
|
-
});
|
|
638
|
-
|
|
639
|
-
test('network --clear clears buffer', async () => {
|
|
640
|
-
const result = await handleReadCommand('network', ['--clear'], bm);
|
|
641
|
-
expect(result).toContain('cleared');
|
|
642
|
-
});
|
|
643
|
-
});
|
|
644
|
-
|
|
645
|
-
// ─── Cookies / Storage ──────────────────────────────────────────
|
|
646
|
-
|
|
647
|
-
describe('Cookies and storage', () => {
|
|
648
|
-
test('cookies returns array', async () => {
|
|
649
|
-
await handleWriteCommand('goto', [baseUrl + '/basic.html'], bm);
|
|
650
|
-
const result = await handleReadCommand('cookies', [], bm);
|
|
651
|
-
// Test server doesn't set cookies, so empty array
|
|
652
|
-
expect(result).toBe('[]');
|
|
653
|
-
});
|
|
654
|
-
|
|
655
|
-
test('storage set and get works', async () => {
|
|
656
|
-
await handleReadCommand('storage', ['set', 'testData', 'testValue'], bm);
|
|
657
|
-
const result = await handleReadCommand('storage', [], bm);
|
|
658
|
-
const storage = JSON.parse(result);
|
|
659
|
-
expect(storage.localStorage.testData).toBe('testValue');
|
|
660
|
-
});
|
|
661
|
-
|
|
662
|
-
test('storage read redacts sensitive keys', async () => {
|
|
663
|
-
await handleWriteCommand('goto', [baseUrl + '/basic.html'], bm);
|
|
664
|
-
await handleReadCommand('storage', ['set', 'auth_token', 'my-secret-token'], bm);
|
|
665
|
-
await handleReadCommand('storage', ['set', 'api_key', 'key-12345'], bm);
|
|
666
|
-
await handleReadCommand('storage', ['set', 'displayName', 'normalValue'], bm);
|
|
667
|
-
const result = await handleReadCommand('storage', [], bm);
|
|
668
|
-
const storage = JSON.parse(result);
|
|
669
|
-
expect(storage.localStorage.auth_token).toMatch(/REDACTED/);
|
|
670
|
-
expect(storage.localStorage.api_key).toMatch(/REDACTED/);
|
|
671
|
-
expect(storage.localStorage.displayName).toBe('normalValue');
|
|
672
|
-
});
|
|
673
|
-
|
|
674
|
-
test('storage read redacts sensitive values by prefix', async () => {
|
|
675
|
-
await handleWriteCommand('goto', [baseUrl + '/basic.html'], bm);
|
|
676
|
-
// JWT value under innocuous key name
|
|
677
|
-
await handleReadCommand('storage', ['set', 'userData', 'eyJhbGciOiJIUzI1NiJ9.payload.sig'], bm);
|
|
678
|
-
// GitHub PAT under innocuous key name
|
|
679
|
-
await handleReadCommand('storage', ['set', 'repoAccess', 'ghp_abc123def456'], bm);
|
|
680
|
-
const result = await handleReadCommand('storage', [], bm);
|
|
681
|
-
const storage = JSON.parse(result);
|
|
682
|
-
expect(storage.localStorage.userData).toMatch(/REDACTED/);
|
|
683
|
-
expect(storage.localStorage.repoAccess).toMatch(/REDACTED/);
|
|
684
|
-
});
|
|
685
|
-
|
|
686
|
-
test('storage redaction includes value length', async () => {
|
|
687
|
-
await handleWriteCommand('goto', [baseUrl + '/basic.html'], bm);
|
|
688
|
-
await handleReadCommand('storage', ['set', 'session_token', 'abc123'], bm);
|
|
689
|
-
const result = await handleReadCommand('storage', [], bm);
|
|
690
|
-
const storage = JSON.parse(result);
|
|
691
|
-
expect(storage.localStorage.session_token).toBe('[REDACTED — 6 chars]');
|
|
692
|
-
});
|
|
693
|
-
});
|
|
694
|
-
|
|
695
|
-
// ─── Performance ────────────────────────────────────────────────
|
|
696
|
-
|
|
697
|
-
describe('Performance', () => {
|
|
698
|
-
test('perf returns timing data', async () => {
|
|
699
|
-
await handleWriteCommand('goto', [baseUrl + '/basic.html'], bm);
|
|
700
|
-
const result = await handleReadCommand('perf', [], bm);
|
|
701
|
-
expect(result).toContain('dns');
|
|
702
|
-
expect(result).toContain('ttfb');
|
|
703
|
-
expect(result).toContain('load');
|
|
704
|
-
expect(result).toContain('ms');
|
|
705
|
-
});
|
|
706
|
-
});
|
|
707
|
-
|
|
708
|
-
// ─── Visual ─────────────────────────────────────────────────────
|
|
709
|
-
|
|
710
|
-
describe('Visual', () => {
|
|
711
|
-
test('screenshot saves file', async () => {
|
|
712
|
-
await handleWriteCommand('goto', [baseUrl + '/basic.html'], bm);
|
|
713
|
-
const screenshotPath = tmpp('browse-test-screenshot.png');
|
|
714
|
-
const result = await handleMetaCommand('screenshot', [screenshotPath], bm, async () => {});
|
|
715
|
-
expect(result).toContain('Screenshot saved');
|
|
716
|
-
expect(fs.existsSync(screenshotPath)).toBe(true);
|
|
717
|
-
const stat = fs.statSync(screenshotPath);
|
|
718
|
-
expect(stat.size).toBeGreaterThan(1000);
|
|
719
|
-
fs.unlinkSync(screenshotPath);
|
|
720
|
-
});
|
|
721
|
-
|
|
722
|
-
test('screenshot --viewport saves viewport-only', async () => {
|
|
723
|
-
await handleWriteCommand('goto', [baseUrl + '/basic.html'], bm);
|
|
724
|
-
const p = tmpp('browse-test-viewport.png');
|
|
725
|
-
const result = await handleMetaCommand('screenshot', ['--viewport', p], bm, async () => {});
|
|
726
|
-
expect(result).toContain('Screenshot saved (viewport)');
|
|
727
|
-
expect(fs.existsSync(p)).toBe(true);
|
|
728
|
-
expect(fs.statSync(p).size).toBeGreaterThan(1000);
|
|
729
|
-
fs.unlinkSync(p);
|
|
730
|
-
});
|
|
731
|
-
|
|
732
|
-
test('screenshot with CSS selector crops to element', async () => {
|
|
733
|
-
await handleWriteCommand('goto', [baseUrl + '/basic.html'], bm);
|
|
734
|
-
const p = tmpp('browse-test-element-css.png');
|
|
735
|
-
const result = await handleMetaCommand('screenshot', ['#title', p], bm, async () => {});
|
|
736
|
-
expect(result).toContain('Screenshot saved (element)');
|
|
737
|
-
expect(fs.existsSync(p)).toBe(true);
|
|
738
|
-
expect(fs.statSync(p).size).toBeGreaterThan(100);
|
|
739
|
-
fs.unlinkSync(p);
|
|
740
|
-
});
|
|
741
|
-
|
|
742
|
-
test('screenshot with @ref crops to element', async () => {
|
|
743
|
-
await handleWriteCommand('goto', [baseUrl + '/basic.html'], bm);
|
|
744
|
-
await handleMetaCommand('snapshot', [], bm, async () => {});
|
|
745
|
-
const p = tmpp('browse-test-element-ref.png');
|
|
746
|
-
const result = await handleMetaCommand('screenshot', ['@e1', p], bm, async () => {});
|
|
747
|
-
expect(result).toContain('Screenshot saved (element)');
|
|
748
|
-
expect(fs.existsSync(p)).toBe(true);
|
|
749
|
-
expect(fs.statSync(p).size).toBeGreaterThan(100);
|
|
750
|
-
fs.unlinkSync(p);
|
|
751
|
-
});
|
|
752
|
-
|
|
753
|
-
test('screenshot --clip crops to region', async () => {
|
|
754
|
-
await handleWriteCommand('goto', [baseUrl + '/basic.html'], bm);
|
|
755
|
-
const p = tmpp('browse-test-clip.png');
|
|
756
|
-
const result = await handleMetaCommand('screenshot', ['--clip', '0,0,100,100', p], bm, async () => {});
|
|
757
|
-
expect(result).toContain('Screenshot saved (clip 0,0,100,100)');
|
|
758
|
-
expect(fs.existsSync(p)).toBe(true);
|
|
759
|
-
expect(fs.statSync(p).size).toBeGreaterThan(100);
|
|
760
|
-
fs.unlinkSync(p);
|
|
761
|
-
});
|
|
762
|
-
|
|
763
|
-
test('screenshot --clip + selector throws', async () => {
|
|
764
|
-
await handleWriteCommand('goto', [baseUrl + '/basic.html'], bm);
|
|
765
|
-
try {
|
|
766
|
-
await handleMetaCommand('screenshot', ['--clip', '0,0,100,100', '#title'], bm, async () => {});
|
|
767
|
-
expect(true).toBe(false);
|
|
768
|
-
} catch (err: any) {
|
|
769
|
-
expect(err.message).toContain('Cannot use --clip with a selector/ref');
|
|
770
|
-
}
|
|
771
|
-
});
|
|
772
|
-
|
|
773
|
-
test('screenshot --viewport + --clip throws', async () => {
|
|
774
|
-
await handleWriteCommand('goto', [baseUrl + '/basic.html'], bm);
|
|
775
|
-
try {
|
|
776
|
-
await handleMetaCommand('screenshot', ['--viewport', '--clip', '0,0,100,100'], bm, async () => {});
|
|
777
|
-
expect(true).toBe(false);
|
|
778
|
-
} catch (err: any) {
|
|
779
|
-
expect(err.message).toContain('Cannot use --viewport with --clip');
|
|
780
|
-
}
|
|
781
|
-
});
|
|
782
|
-
|
|
783
|
-
test('screenshot --clip with invalid coords throws', async () => {
|
|
784
|
-
await handleWriteCommand('goto', [baseUrl + '/basic.html'], bm);
|
|
785
|
-
try {
|
|
786
|
-
await handleMetaCommand('screenshot', ['--clip', 'abc'], bm, async () => {});
|
|
787
|
-
expect(true).toBe(false);
|
|
788
|
-
} catch (err: any) {
|
|
789
|
-
expect(err.message).toContain('all must be numbers');
|
|
790
|
-
}
|
|
791
|
-
});
|
|
792
|
-
|
|
793
|
-
test('screenshot accepts --clip=x,y,w,h and --selector=<css>', async () => {
|
|
794
|
-
await handleWriteCommand('goto', [baseUrl + '/basic.html'], bm);
|
|
795
|
-
const p = tmpp(`browse-test-clip-eq-${Date.now()}.png`);
|
|
796
|
-
try {
|
|
797
|
-
expect(await handleMetaCommand('screenshot', ['--clip=0,0,10,10', p], bm, async () => {})).toContain('Screenshot saved (clip 0,0,10,10)');
|
|
798
|
-
const png = fs.readFileSync(p);
|
|
799
|
-
expect([png.readUInt32BE(16), png.readUInt32BE(20)]).toEqual([10, 10]);
|
|
800
|
-
expect(await handleMetaCommand('screenshot', ['--selector=#title', p], bm, async () => {})).toContain('Screenshot saved (element)');
|
|
801
|
-
} finally {
|
|
802
|
-
fs.rmSync(p, { force: true });
|
|
803
|
-
}
|
|
804
|
-
});
|
|
805
|
-
|
|
806
|
-
test('screenshot --clip= keeps the usage errors and the selector conflict', async () => {
|
|
807
|
-
await handleWriteCommand('goto', [baseUrl + '/basic.html'], bm);
|
|
808
|
-
const run = (args: string[]) => handleMetaCommand('screenshot', args, bm, async () => {});
|
|
809
|
-
await expect(run(['--clip='])).rejects.toThrow('Usage: screenshot --clip x,y,w,h');
|
|
810
|
-
await expect(run(['--clip=1,2,3'])).rejects.toThrow('all must be numbers');
|
|
811
|
-
await expect(run(['--selector='])).rejects.toThrow('Usage: screenshot --selector');
|
|
812
|
-
await expect(run(['--clip=0,0,10,10', '#title'])).rejects.toThrow('Cannot use --clip with a selector/ref');
|
|
813
|
-
await expect(run(['--selector=#title', '--clip=0,0,10,10'])).rejects.toThrow('Cannot use --clip with a selector/ref');
|
|
814
|
-
await expect(run(['--viewport=1'])).rejects.toThrow('Unknown screenshot flag');
|
|
815
|
-
});
|
|
816
|
-
|
|
817
|
-
test('screenshot unknown flag throws', async () => {
|
|
818
|
-
await handleWriteCommand('goto', [baseUrl + '/basic.html'], bm);
|
|
819
|
-
try {
|
|
820
|
-
await handleMetaCommand('screenshot', ['--bogus', tmpp('foo.png')], bm, async () => {});
|
|
821
|
-
expect(true).toBe(false);
|
|
822
|
-
} catch (err: any) {
|
|
823
|
-
expect(err.message).toContain('Unknown screenshot flag');
|
|
824
|
-
}
|
|
825
|
-
});
|
|
826
|
-
|
|
827
|
-
test('screenshot --viewport still validates path', async () => {
|
|
828
|
-
await handleWriteCommand('goto', [baseUrl + '/basic.html'], bm);
|
|
829
|
-
try {
|
|
830
|
-
await handleMetaCommand('screenshot', ['--viewport', '/etc/evil.png'], bm, async () => {});
|
|
831
|
-
expect(true).toBe(false);
|
|
832
|
-
} catch (err: any) {
|
|
833
|
-
expect(err.message).toContain('Path must be within');
|
|
834
|
-
}
|
|
835
|
-
});
|
|
836
|
-
|
|
837
|
-
test('screenshot treats relative dot-slash path as file path, not CSS selector', async () => {
|
|
838
|
-
await handleWriteCommand('goto', [baseUrl + '/basic.html'], bm);
|
|
839
|
-
// ./path/to/file.png must be treated as output path, not a CSS class selector (#495)
|
|
840
|
-
const relPath = './browse-test-dotpath.png';
|
|
841
|
-
const absPath = path.resolve(relPath);
|
|
842
|
-
const result = await handleMetaCommand('screenshot', [relPath], bm, async () => {});
|
|
843
|
-
expect(result).toContain('Screenshot saved');
|
|
844
|
-
expect(fs.existsSync(absPath)).toBe(true);
|
|
845
|
-
fs.unlinkSync(absPath);
|
|
846
|
-
});
|
|
847
|
-
|
|
848
|
-
test('screenshot with nonexistent selector throws timeout', async () => {
|
|
849
|
-
await handleWriteCommand('goto', [baseUrl + '/basic.html'], bm);
|
|
850
|
-
try {
|
|
851
|
-
await handleMetaCommand('screenshot', ['.nonexistent-element-xyz'], bm, async () => {});
|
|
852
|
-
expect(true).toBe(false);
|
|
853
|
-
} catch (err: any) {
|
|
854
|
-
expect(err.message).toBeDefined();
|
|
855
|
-
}
|
|
856
|
-
}, 10000);
|
|
857
|
-
|
|
858
|
-
test('responsive saves 3 screenshots', async () => {
|
|
859
|
-
await handleWriteCommand('goto', [baseUrl + '/responsive.html'], bm);
|
|
860
|
-
const prefix = tmpp('browse-test-resp');
|
|
861
|
-
const result = await handleMetaCommand('responsive', [prefix], bm, async () => {});
|
|
862
|
-
expect(result).toContain('mobile');
|
|
863
|
-
expect(result).toContain('tablet');
|
|
864
|
-
expect(result).toContain('desktop');
|
|
865
|
-
|
|
866
|
-
expect(fs.existsSync(`${prefix}-mobile.png`)).toBe(true);
|
|
867
|
-
expect(fs.existsSync(`${prefix}-tablet.png`)).toBe(true);
|
|
868
|
-
expect(fs.existsSync(`${prefix}-desktop.png`)).toBe(true);
|
|
869
|
-
|
|
870
|
-
// Cleanup
|
|
871
|
-
fs.unlinkSync(`${prefix}-mobile.png`);
|
|
872
|
-
fs.unlinkSync(`${prefix}-tablet.png`);
|
|
873
|
-
fs.unlinkSync(`${prefix}-desktop.png`);
|
|
874
|
-
});
|
|
875
|
-
});
|
|
876
|
-
|
|
877
|
-
// ─── Tabs ───────────────────────────────────────────────────────
|
|
878
|
-
|
|
879
|
-
describe('Tabs', () => {
|
|
880
|
-
test('tabs lists all tabs', async () => {
|
|
881
|
-
const result = await handleMetaCommand('tabs', [], bm, async () => {});
|
|
882
|
-
expect(result).toContain('[');
|
|
883
|
-
expect(result).toContain(']');
|
|
884
|
-
});
|
|
885
|
-
|
|
886
|
-
test('newtab opens new tab', async () => {
|
|
887
|
-
const result = await handleMetaCommand('newtab', [baseUrl + '/forms.html'], bm, async () => {});
|
|
888
|
-
expect(result).toContain('Opened tab');
|
|
889
|
-
|
|
890
|
-
const tabCount = bm.getTabCount();
|
|
891
|
-
expect(tabCount).toBeGreaterThanOrEqual(2);
|
|
892
|
-
});
|
|
893
|
-
|
|
894
|
-
test('tab switches to specific tab', async () => {
|
|
895
|
-
const result = await handleMetaCommand('tab', ['1'], bm, async () => {});
|
|
896
|
-
expect(result).toContain('Switched to tab 1');
|
|
897
|
-
});
|
|
898
|
-
|
|
899
|
-
test('closetab closes a tab', async () => {
|
|
900
|
-
const before = bm.getTabCount();
|
|
901
|
-
// Close the last opened tab
|
|
902
|
-
const tabs = await bm.getTabListWithTitles();
|
|
903
|
-
const lastTab = tabs[tabs.length - 1];
|
|
904
|
-
const result = await handleMetaCommand('closetab', [String(lastTab.id)], bm, async () => {});
|
|
905
|
-
expect(result).toContain('Closed tab');
|
|
906
|
-
expect(bm.getTabCount()).toBe(before - 1);
|
|
907
|
-
});
|
|
908
|
-
});
|
|
909
|
-
|
|
910
|
-
// ─── Diff ───────────────────────────────────────────────────────
|
|
911
|
-
|
|
912
|
-
describe('Diff', () => {
|
|
913
|
-
test('diff shows differences between pages', async () => {
|
|
914
|
-
const result = await handleMetaCommand(
|
|
915
|
-
'diff',
|
|
916
|
-
[baseUrl + '/basic.html', baseUrl + '/forms.html'],
|
|
917
|
-
bm,
|
|
918
|
-
async () => {}
|
|
919
|
-
);
|
|
920
|
-
expect(result).toContain('---');
|
|
921
|
-
expect(result).toContain('+++');
|
|
922
|
-
// basic.html has "Hello World", forms.html has "Form Test Page"
|
|
923
|
-
expect(result).toContain('Hello World');
|
|
924
|
-
expect(result).toContain('Form Test Page');
|
|
925
|
-
});
|
|
926
|
-
});
|
|
927
|
-
|
|
928
|
-
// ─── Chain ──────────────────────────────────────────────────────
|
|
929
|
-
|
|
930
|
-
describe('Chain', () => {
|
|
931
|
-
test('chain executes sequence of commands', async () => {
|
|
932
|
-
const commands = JSON.stringify([
|
|
933
|
-
['goto', baseUrl + '/basic.html'],
|
|
934
|
-
['js', 'document.title'],
|
|
935
|
-
['css', 'h1', 'color'],
|
|
936
|
-
]);
|
|
937
|
-
const result = await chainMeta(bm, [commands]);
|
|
938
|
-
expect(result).toContain('[goto]');
|
|
939
|
-
expect(result).toContain('Test Page - Basic');
|
|
940
|
-
expect(result).toContain('[css]');
|
|
941
|
-
});
|
|
942
|
-
|
|
943
|
-
test('chain wraps page-content sub-commands with trust markers', async () => {
|
|
944
|
-
await handleWriteCommand('goto', [baseUrl + '/basic.html'], bm);
|
|
945
|
-
const result = await chainMeta(bm, ['text']);
|
|
946
|
-
expect(result).toContain('BEGIN UNTRUSTED EXTERNAL CONTENT');
|
|
947
|
-
expect(result).toContain('END UNTRUSTED EXTERNAL CONTENT');
|
|
948
|
-
});
|
|
949
|
-
|
|
950
|
-
test('chain reports real error when write command fails', async () => {
|
|
951
|
-
const commands = JSON.stringify([
|
|
952
|
-
['goto', 'http://localhost:1/unreachable'],
|
|
953
|
-
]);
|
|
954
|
-
const result = await chainMeta(bm, [commands]);
|
|
955
|
-
expect(result).toContain('[goto] ERROR:');
|
|
956
|
-
expect(result).not.toContain('Unknown meta command');
|
|
957
|
-
expect(result).not.toContain('Unknown read command');
|
|
958
|
-
});
|
|
959
|
-
});
|
|
960
|
-
|
|
961
|
-
// ─── Status ─────────────────────────────────────────────────────
|
|
962
|
-
|
|
963
|
-
describe('Status', () => {
|
|
964
|
-
test('status reports health', async () => {
|
|
965
|
-
const result = await handleMetaCommand('status', [], bm, async () => {});
|
|
966
|
-
expect(result).toContain('Status: healthy');
|
|
967
|
-
expect(result).toContain('Tabs:');
|
|
968
|
-
});
|
|
969
|
-
});
|
|
970
|
-
|
|
971
|
-
// ─── CLI server script resolution ───────────────────────────────
|
|
972
|
-
|
|
973
|
-
describe('CLI server script resolution', () => {
|
|
974
|
-
test('prefers adjacent browse/src/server.ts for compiled project installs', () => {
|
|
975
|
-
const root = fs.mkdtempSync(tmpp('gstack-cli-'));
|
|
976
|
-
const execPath = path.join(root, '.claude/skills/gstack/browse/dist/browse');
|
|
977
|
-
const serverPath = path.join(root, '.claude/skills/gstack/browse/src/server.ts');
|
|
978
|
-
|
|
979
|
-
fs.mkdirSync(path.dirname(execPath), { recursive: true });
|
|
980
|
-
fs.mkdirSync(path.dirname(serverPath), { recursive: true });
|
|
981
|
-
fs.writeFileSync(serverPath, '// test server\n');
|
|
982
|
-
|
|
983
|
-
const resolved = resolveServerScript(
|
|
984
|
-
{ HOME: path.join(root, 'empty-home') },
|
|
985
|
-
'$bunfs/root',
|
|
986
|
-
execPath
|
|
987
|
-
);
|
|
988
|
-
|
|
989
|
-
expect(resolved).toBe(serverPath);
|
|
990
|
-
|
|
991
|
-
fs.rmSync(root, { recursive: true, force: true });
|
|
992
|
-
});
|
|
993
|
-
});
|
|
994
|
-
|
|
995
|
-
// ─── CLI lifecycle ──────────────────────────────────────────────
|
|
996
|
-
|
|
997
|
-
describe('CLI lifecycle', () => {
|
|
998
|
-
test('dead state file triggers a clean restart', async () => {
|
|
999
|
-
const stateFile = tmpp(`browse-test-state-${Date.now()}.json`);
|
|
1000
|
-
fs.writeFileSync(stateFile, JSON.stringify({
|
|
1001
|
-
port: 1,
|
|
1002
|
-
token: 'fake',
|
|
1003
|
-
pid: 999999,
|
|
1004
|
-
}));
|
|
1005
|
-
|
|
1006
|
-
const cliPath = path.resolve(__dirname, '../src/cli.ts');
|
|
1007
|
-
const cliEnv: Record<string, string> = {};
|
|
1008
|
-
for (const [k, v] of Object.entries(process.env)) {
|
|
1009
|
-
if (v !== undefined) cliEnv[k] = v;
|
|
1010
|
-
}
|
|
1011
|
-
cliEnv.BROWSE_STATE_FILE = stateFile;
|
|
1012
|
-
const result = await new Promise<{ code: number; stdout: string; stderr: string }>((resolve) => {
|
|
1013
|
-
const proc = spawn('bun', ['run', cliPath, 'status'], {
|
|
1014
|
-
// Must exceed the CLI's startup budget (resolveStartTimeout, 15s
|
|
1015
|
-
// non-CI POSIX) or a slow cold boot under full-suite load gets the
|
|
1016
|
-
// child killed at the exact moment the CLI would have succeeded.
|
|
1017
|
-
timeout: 18000,
|
|
1018
|
-
env: cliEnv,
|
|
1019
|
-
});
|
|
1020
|
-
let stdout = '';
|
|
1021
|
-
let stderr = '';
|
|
1022
|
-
proc.stdout.on('data', (d) => stdout += d.toString());
|
|
1023
|
-
proc.stderr.on('data', (d) => stderr += d.toString());
|
|
1024
|
-
proc.on('close', (code) => resolve({ code: code ?? 1, stdout, stderr }));
|
|
1025
|
-
});
|
|
1026
|
-
|
|
1027
|
-
let restartedPid: number | null = null;
|
|
1028
|
-
if (fs.existsSync(stateFile)) {
|
|
1029
|
-
restartedPid = JSON.parse(fs.readFileSync(stateFile, 'utf-8')).pid;
|
|
1030
|
-
fs.unlinkSync(stateFile);
|
|
1031
|
-
}
|
|
1032
|
-
let daemonSurvivedStop = false;
|
|
1033
|
-
if (restartedPid) {
|
|
1034
|
-
try { process.kill(restartedPid, 'SIGINT'); } catch {}
|
|
1035
|
-
const deadline = Date.now() + 5000;
|
|
1036
|
-
while (Date.now() < deadline && isProcessAlive(restartedPid)) await Bun.sleep(50);
|
|
1037
|
-
daemonSurvivedStop = isProcessAlive(restartedPid);
|
|
1038
|
-
if (daemonSurvivedStop) try { process.kill(restartedPid, 'SIGKILL'); } catch {}
|
|
1039
|
-
}
|
|
1040
|
-
|
|
1041
|
-
expect(result.code).toBe(0);
|
|
1042
|
-
expect(result.stdout).toContain('Status: healthy');
|
|
1043
|
-
expect(result.stderr).toContain('Starting server');
|
|
1044
|
-
expect(restartedPid).toBeGreaterThan(0);
|
|
1045
|
-
expect(daemonSurvivedStop).toBe(false);
|
|
1046
|
-
}, 20000);
|
|
1047
|
-
});
|
|
1048
|
-
|
|
1049
|
-
// ─── Buffer bounds ──────────────────────────────────────────────
|
|
1050
|
-
|
|
1051
|
-
describe('Buffer bounds', () => {
|
|
1052
|
-
test('console buffer caps at 50000 entries', () => {
|
|
1053
|
-
consoleBuffer.clear();
|
|
1054
|
-
for (let i = 0; i < 50_010; i++) {
|
|
1055
|
-
addConsoleEntry({ timestamp: i, level: 'log', text: `msg-${i}` });
|
|
1056
|
-
}
|
|
1057
|
-
expect(consoleBuffer.length).toBe(50_000);
|
|
1058
|
-
const entries = consoleBuffer.toArray();
|
|
1059
|
-
expect(entries[0].text).toBe('msg-10');
|
|
1060
|
-
expect(entries[entries.length - 1].text).toBe('msg-50009');
|
|
1061
|
-
consoleBuffer.clear();
|
|
1062
|
-
});
|
|
1063
|
-
|
|
1064
|
-
test('network buffer caps at 50000 entries', () => {
|
|
1065
|
-
networkBuffer.clear();
|
|
1066
|
-
for (let i = 0; i < 50_010; i++) {
|
|
1067
|
-
addNetworkEntry({ timestamp: i, method: 'GET', url: `http://x/${i}` });
|
|
1068
|
-
}
|
|
1069
|
-
expect(networkBuffer.length).toBe(50_000);
|
|
1070
|
-
const entries = networkBuffer.toArray();
|
|
1071
|
-
expect(entries[0].url).toBe('http://x/10');
|
|
1072
|
-
expect(entries[entries.length - 1].url).toBe('http://x/50009');
|
|
1073
|
-
networkBuffer.clear();
|
|
1074
|
-
});
|
|
1075
|
-
|
|
1076
|
-
test('totalAdded counters keep incrementing past buffer cap', () => {
|
|
1077
|
-
const startConsole = consoleBuffer.totalAdded;
|
|
1078
|
-
const startNetwork = networkBuffer.totalAdded;
|
|
1079
|
-
for (let i = 0; i < 100; i++) {
|
|
1080
|
-
addConsoleEntry({ timestamp: i, level: 'log', text: `t-${i}` });
|
|
1081
|
-
addNetworkEntry({ timestamp: i, method: 'GET', url: `http://t/${i}` });
|
|
1082
|
-
}
|
|
1083
|
-
expect(consoleBuffer.totalAdded).toBe(startConsole + 100);
|
|
1084
|
-
expect(networkBuffer.totalAdded).toBe(startNetwork + 100);
|
|
1085
|
-
consoleBuffer.clear();
|
|
1086
|
-
networkBuffer.clear();
|
|
1087
|
-
});
|
|
1088
|
-
});
|
|
1089
|
-
|
|
1090
|
-
// ─── CircularBuffer Unit Tests ─────────────────────────────────
|
|
1091
|
-
|
|
1092
|
-
describe('CircularBuffer', () => {
|
|
1093
|
-
test('push and toArray return items in insertion order', () => {
|
|
1094
|
-
const buf = new CircularBuffer<number>(5);
|
|
1095
|
-
buf.push(1); buf.push(2); buf.push(3);
|
|
1096
|
-
expect(buf.toArray()).toEqual([1, 2, 3]);
|
|
1097
|
-
expect(buf.length).toBe(3);
|
|
1098
|
-
});
|
|
1099
|
-
|
|
1100
|
-
test('overwrites oldest when full', () => {
|
|
1101
|
-
const buf = new CircularBuffer<number>(3);
|
|
1102
|
-
buf.push(1); buf.push(2); buf.push(3); buf.push(4);
|
|
1103
|
-
expect(buf.toArray()).toEqual([2, 3, 4]);
|
|
1104
|
-
expect(buf.length).toBe(3);
|
|
1105
|
-
});
|
|
1106
|
-
|
|
1107
|
-
test('totalAdded increments past capacity', () => {
|
|
1108
|
-
const buf = new CircularBuffer<number>(2);
|
|
1109
|
-
buf.push(1); buf.push(2); buf.push(3); buf.push(4); buf.push(5);
|
|
1110
|
-
expect(buf.totalAdded).toBe(5);
|
|
1111
|
-
expect(buf.length).toBe(2);
|
|
1112
|
-
expect(buf.toArray()).toEqual([4, 5]);
|
|
1113
|
-
});
|
|
1114
|
-
|
|
1115
|
-
test('last(n) returns most recent entries', () => {
|
|
1116
|
-
const buf = new CircularBuffer<number>(5);
|
|
1117
|
-
for (let i = 1; i <= 5; i++) buf.push(i);
|
|
1118
|
-
expect(buf.last(3)).toEqual([3, 4, 5]);
|
|
1119
|
-
expect(buf.last(10)).toEqual([1, 2, 3, 4, 5]); // clamped
|
|
1120
|
-
expect(buf.last(1)).toEqual([5]);
|
|
1121
|
-
});
|
|
1122
|
-
|
|
1123
|
-
test('get and set work by index', () => {
|
|
1124
|
-
const buf = new CircularBuffer<string>(3);
|
|
1125
|
-
buf.push('a'); buf.push('b'); buf.push('c');
|
|
1126
|
-
expect(buf.get(0)).toBe('a');
|
|
1127
|
-
expect(buf.get(2)).toBe('c');
|
|
1128
|
-
buf.set(1, 'B');
|
|
1129
|
-
expect(buf.get(1)).toBe('B');
|
|
1130
|
-
expect(buf.get(-1)).toBeUndefined();
|
|
1131
|
-
expect(buf.get(5)).toBeUndefined();
|
|
1132
|
-
});
|
|
1133
|
-
|
|
1134
|
-
test('clear resets size but not totalAdded', () => {
|
|
1135
|
-
const buf = new CircularBuffer<number>(5);
|
|
1136
|
-
buf.push(1); buf.push(2); buf.push(3);
|
|
1137
|
-
buf.clear();
|
|
1138
|
-
expect(buf.length).toBe(0);
|
|
1139
|
-
expect(buf.totalAdded).toBe(3);
|
|
1140
|
-
expect(buf.toArray()).toEqual([]);
|
|
1141
|
-
});
|
|
1142
|
-
|
|
1143
|
-
test('works with capacity=1', () => {
|
|
1144
|
-
const buf = new CircularBuffer<number>(1);
|
|
1145
|
-
buf.push(10);
|
|
1146
|
-
expect(buf.toArray()).toEqual([10]);
|
|
1147
|
-
buf.push(20);
|
|
1148
|
-
expect(buf.toArray()).toEqual([20]);
|
|
1149
|
-
expect(buf.totalAdded).toBe(2);
|
|
1150
|
-
});
|
|
1151
|
-
});
|
|
1152
|
-
|
|
1153
|
-
// ─── Dialog Handling ─────────────────────────────────────────
|
|
1154
|
-
|
|
1155
|
-
describe('Dialog handling', () => {
|
|
1156
|
-
test('alert does not hang — auto-accepted', async () => {
|
|
1157
|
-
await handleWriteCommand('goto', [baseUrl + '/dialog.html'], bm);
|
|
1158
|
-
await handleWriteCommand('click', ['#alert-btn'], bm);
|
|
1159
|
-
// If we get here, dialog was handled (no hang)
|
|
1160
|
-
const result = await handleReadCommand('dialog', [], bm);
|
|
1161
|
-
expect(result).toContain('alert');
|
|
1162
|
-
expect(result).toContain('Hello from alert');
|
|
1163
|
-
expect(result).toContain('accepted');
|
|
1164
|
-
});
|
|
1165
|
-
|
|
1166
|
-
test('confirm is auto-accepted by default', async () => {
|
|
1167
|
-
await handleWriteCommand('goto', [baseUrl + '/dialog.html'], bm);
|
|
1168
|
-
await handleWriteCommand('click', ['#confirm-btn'], bm);
|
|
1169
|
-
// Wait for DOM update
|
|
1170
|
-
await new Promise(r => setTimeout(r, 100));
|
|
1171
|
-
const result = await handleReadCommand('js', ['document.querySelector("#confirm-result").textContent'], bm);
|
|
1172
|
-
expect(result).toBe('confirmed');
|
|
1173
|
-
});
|
|
1174
|
-
|
|
1175
|
-
test('dialog-dismiss changes behavior', async () => {
|
|
1176
|
-
const setResult = await handleWriteCommand('dialog-dismiss', [], bm);
|
|
1177
|
-
expect(setResult).toContain('dismissed');
|
|
1178
|
-
|
|
1179
|
-
await handleWriteCommand('goto', [baseUrl + '/dialog.html'], bm);
|
|
1180
|
-
await handleWriteCommand('click', ['#confirm-btn'], bm);
|
|
1181
|
-
await new Promise(r => setTimeout(r, 100));
|
|
1182
|
-
const result = await handleReadCommand('js', ['document.querySelector("#confirm-result").textContent'], bm);
|
|
1183
|
-
expect(result).toBe('cancelled');
|
|
1184
|
-
|
|
1185
|
-
// Reset to accept
|
|
1186
|
-
await handleWriteCommand('dialog-accept', [], bm);
|
|
1187
|
-
});
|
|
1188
|
-
|
|
1189
|
-
test('dialog-accept with text provides prompt response', async () => {
|
|
1190
|
-
const setResult = await handleWriteCommand('dialog-accept', ['TestUser'], bm);
|
|
1191
|
-
expect(setResult).toContain('TestUser');
|
|
1192
|
-
|
|
1193
|
-
await handleWriteCommand('goto', [baseUrl + '/dialog.html'], bm);
|
|
1194
|
-
await handleWriteCommand('click', ['#prompt-btn'], bm);
|
|
1195
|
-
await new Promise(r => setTimeout(r, 100));
|
|
1196
|
-
const result = await handleReadCommand('js', ['document.querySelector("#prompt-result").textContent'], bm);
|
|
1197
|
-
expect(result).toBe('TestUser');
|
|
1198
|
-
|
|
1199
|
-
// Reset
|
|
1200
|
-
await handleWriteCommand('dialog-accept', [], bm);
|
|
1201
|
-
});
|
|
1202
|
-
|
|
1203
|
-
test('dialog --clear clears buffer', async () => {
|
|
1204
|
-
const cleared = await handleReadCommand('dialog', ['--clear'], bm);
|
|
1205
|
-
expect(cleared).toContain('cleared');
|
|
1206
|
-
const after = await handleReadCommand('dialog', [], bm);
|
|
1207
|
-
expect(after).toContain('no dialogs');
|
|
1208
|
-
});
|
|
1209
|
-
});
|
|
1210
|
-
|
|
1211
|
-
// ─── Element State Checks (is) ─────────────────────────────────
|
|
1212
|
-
|
|
1213
|
-
describe('Element state checks', () => {
|
|
1214
|
-
beforeAll(async () => {
|
|
1215
|
-
await handleWriteCommand('goto', [baseUrl + '/states.html'], bm);
|
|
1216
|
-
});
|
|
1217
|
-
|
|
1218
|
-
test('is visible returns true for visible element', async () => {
|
|
1219
|
-
const result = await handleReadCommand('is', ['visible', '#visible-div'], bm);
|
|
1220
|
-
expect(result).toBe('true');
|
|
1221
|
-
});
|
|
1222
|
-
|
|
1223
|
-
test('is hidden returns true for hidden element', async () => {
|
|
1224
|
-
const result = await handleReadCommand('is', ['hidden', '#hidden-div'], bm);
|
|
1225
|
-
expect(result).toBe('true');
|
|
1226
|
-
});
|
|
1227
|
-
|
|
1228
|
-
test('is visible returns false for hidden element', async () => {
|
|
1229
|
-
const result = await handleReadCommand('is', ['visible', '#hidden-div'], bm);
|
|
1230
|
-
expect(result).toBe('false');
|
|
1231
|
-
});
|
|
1232
|
-
|
|
1233
|
-
test('is enabled returns true for enabled input', async () => {
|
|
1234
|
-
const result = await handleReadCommand('is', ['enabled', '#enabled-input'], bm);
|
|
1235
|
-
expect(result).toBe('true');
|
|
1236
|
-
});
|
|
1237
|
-
|
|
1238
|
-
test('is disabled returns true for disabled input', async () => {
|
|
1239
|
-
const result = await handleReadCommand('is', ['disabled', '#disabled-input'], bm);
|
|
1240
|
-
expect(result).toBe('true');
|
|
1241
|
-
});
|
|
1242
|
-
|
|
1243
|
-
test('is checked returns true for checked checkbox', async () => {
|
|
1244
|
-
const result = await handleReadCommand('is', ['checked', '#checked-box'], bm);
|
|
1245
|
-
expect(result).toBe('true');
|
|
1246
|
-
});
|
|
1247
|
-
|
|
1248
|
-
test('is checked returns false for unchecked checkbox', async () => {
|
|
1249
|
-
const result = await handleReadCommand('is', ['checked', '#unchecked-box'], bm);
|
|
1250
|
-
expect(result).toBe('false');
|
|
1251
|
-
});
|
|
1252
|
-
|
|
1253
|
-
test('is editable returns true for normal input', async () => {
|
|
1254
|
-
const result = await handleReadCommand('is', ['editable', '#enabled-input'], bm);
|
|
1255
|
-
expect(result).toBe('true');
|
|
1256
|
-
});
|
|
1257
|
-
|
|
1258
|
-
test('is editable returns false for readonly input', async () => {
|
|
1259
|
-
const result = await handleReadCommand('is', ['editable', '#readonly-input'], bm);
|
|
1260
|
-
expect(result).toBe('false');
|
|
1261
|
-
});
|
|
1262
|
-
|
|
1263
|
-
test('is focused after click', async () => {
|
|
1264
|
-
await handleWriteCommand('click', ['#enabled-input'], bm);
|
|
1265
|
-
const result = await handleReadCommand('is', ['focused', '#enabled-input'], bm);
|
|
1266
|
-
expect(result).toBe('true');
|
|
1267
|
-
});
|
|
1268
|
-
|
|
1269
|
-
test('is with @ref works', async () => {
|
|
1270
|
-
await handleMetaCommand('snapshot', ['-i'], bm, async () => {});
|
|
1271
|
-
// Find a ref for the enabled input
|
|
1272
|
-
const snap = await handleMetaCommand('snapshot', ['-i'], bm, async () => {});
|
|
1273
|
-
const textboxLine = snap.split('\n').find(l => l.includes('[textbox]'));
|
|
1274
|
-
if (textboxLine) {
|
|
1275
|
-
const refMatch = textboxLine.match(/@(e\d+)/);
|
|
1276
|
-
if (refMatch) {
|
|
1277
|
-
const ref = `@${refMatch[1]}`;
|
|
1278
|
-
const result = await handleReadCommand('is', ['visible', ref], bm);
|
|
1279
|
-
expect(result).toBe('true');
|
|
1280
|
-
}
|
|
1281
|
-
}
|
|
1282
|
-
});
|
|
1283
|
-
|
|
1284
|
-
test('is with unknown property throws', async () => {
|
|
1285
|
-
try {
|
|
1286
|
-
await handleReadCommand('is', ['bogus', '#enabled-input'], bm);
|
|
1287
|
-
expect(true).toBe(false);
|
|
1288
|
-
} catch (err: any) {
|
|
1289
|
-
expect(err.message).toContain('Unknown property');
|
|
1290
|
-
}
|
|
1291
|
-
});
|
|
1292
|
-
|
|
1293
|
-
test('is with missing args throws', async () => {
|
|
1294
|
-
try {
|
|
1295
|
-
await handleReadCommand('is', ['visible'], bm);
|
|
1296
|
-
expect(true).toBe(false);
|
|
1297
|
-
} catch (err: any) {
|
|
1298
|
-
expect(err.message).toContain('Usage');
|
|
1299
|
-
}
|
|
1300
|
-
});
|
|
1301
|
-
});
|
|
1302
|
-
|
|
1303
|
-
// ─── File Upload ─────────────────────────────────────────────────
|
|
1304
|
-
|
|
1305
|
-
describe('File upload', () => {
|
|
1306
|
-
test('upload single file', async () => {
|
|
1307
|
-
await handleWriteCommand('goto', [baseUrl + '/upload.html'], bm);
|
|
1308
|
-
// Create a temp file to upload
|
|
1309
|
-
const tempFile = tmpp('browse-test-upload.txt');
|
|
1310
|
-
fs.writeFileSync(tempFile, 'test content');
|
|
1311
|
-
const result = await handleWriteCommand('upload', ['#file-input', tempFile], bm);
|
|
1312
|
-
expect(result).toContain('Uploaded');
|
|
1313
|
-
expect(result).toContain('browse-test-upload.txt');
|
|
1314
|
-
|
|
1315
|
-
// Verify upload handler fired
|
|
1316
|
-
await new Promise(r => setTimeout(r, 100));
|
|
1317
|
-
const text = await handleReadCommand('js', ['document.querySelector("#upload-result").textContent'], bm);
|
|
1318
|
-
expect(text).toContain('browse-test-upload.txt');
|
|
1319
|
-
fs.unlinkSync(tempFile);
|
|
1320
|
-
});
|
|
1321
|
-
|
|
1322
|
-
test('upload with @ref works', async () => {
|
|
1323
|
-
await handleWriteCommand('goto', [baseUrl + '/upload.html'], bm);
|
|
1324
|
-
const tempFile = tmpp('browse-test-upload2.txt');
|
|
1325
|
-
fs.writeFileSync(tempFile, 'ref upload test');
|
|
1326
|
-
const snap = await handleMetaCommand('snapshot', ['-i'], bm, async () => {});
|
|
1327
|
-
// Find the file input ref (it won't appear as "file input" in aria — use CSS selector instead)
|
|
1328
|
-
const result = await handleWriteCommand('upload', ['#file-input', tempFile], bm);
|
|
1329
|
-
expect(result).toContain('Uploaded');
|
|
1330
|
-
fs.unlinkSync(tempFile);
|
|
1331
|
-
});
|
|
1332
|
-
|
|
1333
|
-
test('upload nonexistent file throws', async () => {
|
|
1334
|
-
await handleWriteCommand('goto', [baseUrl + '/upload.html'], bm);
|
|
1335
|
-
try {
|
|
1336
|
-
await handleWriteCommand('upload', ['#file-input', tmpp('nonexistent-file-12345.txt')], bm);
|
|
1337
|
-
expect(true).toBe(false);
|
|
1338
|
-
} catch (err: any) {
|
|
1339
|
-
expect(err.message).toContain('File not found');
|
|
1340
|
-
}
|
|
1341
|
-
});
|
|
1342
|
-
|
|
1343
|
-
test('upload missing args throws', async () => {
|
|
1344
|
-
try {
|
|
1345
|
-
await handleWriteCommand('upload', ['#file-input'], bm);
|
|
1346
|
-
expect(true).toBe(false);
|
|
1347
|
-
} catch (err: any) {
|
|
1348
|
-
expect(err.message).toContain('Usage');
|
|
1349
|
-
}
|
|
1350
|
-
});
|
|
1351
|
-
});
|
|
1352
|
-
|
|
1353
|
-
// ─── Eval command ───────────────────────────────────────────────
|
|
1354
|
-
|
|
1355
|
-
describe('Eval', () => {
|
|
1356
|
-
test('eval runs JS file', async () => {
|
|
1357
|
-
await handleWriteCommand('goto', [baseUrl + '/basic.html'], bm);
|
|
1358
|
-
const tempFile = tmpp('browse-test-eval.js');
|
|
1359
|
-
fs.writeFileSync(tempFile, 'document.title + " — evaluated"');
|
|
1360
|
-
const result = await handleReadCommand('eval', [tempFile], bm);
|
|
1361
|
-
expect(result).toBe('Test Page - Basic — evaluated');
|
|
1362
|
-
fs.unlinkSync(tempFile);
|
|
1363
|
-
});
|
|
1364
|
-
|
|
1365
|
-
test('eval returns object as JSON', async () => {
|
|
1366
|
-
const tempFile = tmpp('browse-test-eval-obj.js');
|
|
1367
|
-
fs.writeFileSync(tempFile, '({title: document.title, keys: Object.keys(document.body.dataset)})');
|
|
1368
|
-
const result = await handleReadCommand('eval', [tempFile], bm);
|
|
1369
|
-
const obj = JSON.parse(result);
|
|
1370
|
-
expect(obj.title).toBe('Test Page - Basic');
|
|
1371
|
-
expect(Array.isArray(obj.keys)).toBe(true);
|
|
1372
|
-
fs.unlinkSync(tempFile);
|
|
1373
|
-
});
|
|
1374
|
-
|
|
1375
|
-
test('eval file not found throws', async () => {
|
|
1376
|
-
try {
|
|
1377
|
-
await handleReadCommand('eval', [tmpp('nonexistent-eval.js')], bm);
|
|
1378
|
-
expect(true).toBe(false);
|
|
1379
|
-
} catch (err: any) {
|
|
1380
|
-
expect(err.message).toContain('File not found');
|
|
1381
|
-
}
|
|
1382
|
-
});
|
|
1383
|
-
|
|
1384
|
-
test('eval no arg throws', async () => {
|
|
1385
|
-
try {
|
|
1386
|
-
await handleReadCommand('eval', [], bm);
|
|
1387
|
-
expect(true).toBe(false);
|
|
1388
|
-
} catch (err: any) {
|
|
1389
|
-
expect(err.message).toContain('Usage');
|
|
1390
|
-
}
|
|
1391
|
-
});
|
|
1392
|
-
});
|
|
1393
|
-
|
|
1394
|
-
// ─── Press command ──────────────────────────────────────────────
|
|
1395
|
-
|
|
1396
|
-
describe('Press', () => {
|
|
1397
|
-
test('press Tab moves focus', async () => {
|
|
1398
|
-
await handleWriteCommand('goto', [baseUrl + '/forms.html'], bm);
|
|
1399
|
-
await handleWriteCommand('click', ['#email'], bm);
|
|
1400
|
-
const result = await handleWriteCommand('press', ['Tab'], bm);
|
|
1401
|
-
expect(result).toContain('Pressed Tab');
|
|
1402
|
-
});
|
|
1403
|
-
|
|
1404
|
-
test('press no arg throws', async () => {
|
|
1405
|
-
try {
|
|
1406
|
-
await handleWriteCommand('press', [], bm);
|
|
1407
|
-
expect(true).toBe(false);
|
|
1408
|
-
} catch (err: any) {
|
|
1409
|
-
expect(err.message).toContain('Usage');
|
|
1410
|
-
}
|
|
1411
|
-
});
|
|
1412
|
-
});
|
|
1413
|
-
|
|
1414
|
-
// ─── Cookie command ─────────────────────────────────────────────
|
|
1415
|
-
|
|
1416
|
-
describe('Cookie command', () => {
|
|
1417
|
-
test('cookie sets value', async () => {
|
|
1418
|
-
await handleWriteCommand('goto', [baseUrl + '/basic.html'], bm);
|
|
1419
|
-
const result = await handleWriteCommand('cookie', ['testcookie=testvalue'], bm);
|
|
1420
|
-
expect(result).toContain('Cookie set');
|
|
1421
|
-
|
|
1422
|
-
const cookies = await handleReadCommand('cookies', [], bm);
|
|
1423
|
-
expect(cookies).toContain('testcookie');
|
|
1424
|
-
expect(cookies).toContain('testvalue');
|
|
1425
|
-
});
|
|
1426
|
-
|
|
1427
|
-
test('cookie no arg throws', async () => {
|
|
1428
|
-
try {
|
|
1429
|
-
await handleWriteCommand('cookie', [], bm);
|
|
1430
|
-
expect(true).toBe(false);
|
|
1431
|
-
} catch (err: any) {
|
|
1432
|
-
expect(err.message).toContain('Usage');
|
|
1433
|
-
}
|
|
1434
|
-
});
|
|
1435
|
-
|
|
1436
|
-
test('cookie no = throws', async () => {
|
|
1437
|
-
try {
|
|
1438
|
-
await handleWriteCommand('cookie', ['invalid'], bm);
|
|
1439
|
-
expect(true).toBe(false);
|
|
1440
|
-
} catch (err: any) {
|
|
1441
|
-
expect(err.message).toContain('Usage');
|
|
1442
|
-
}
|
|
1443
|
-
});
|
|
1444
|
-
});
|
|
1445
|
-
|
|
1446
|
-
// ─── Header command ─────────────────────────────────────────────
|
|
1447
|
-
|
|
1448
|
-
describe('Header command', () => {
|
|
1449
|
-
test('header sets value and is sent', async () => {
|
|
1450
|
-
const result = await handleWriteCommand('header', ['X-Test:test-value'], bm);
|
|
1451
|
-
expect(result).toContain('Header set');
|
|
1452
|
-
|
|
1453
|
-
await handleWriteCommand('goto', [baseUrl + '/echo'], bm);
|
|
1454
|
-
const echoText = await handleReadCommand('text', [], bm);
|
|
1455
|
-
expect(echoText).toContain('x-test');
|
|
1456
|
-
expect(echoText).toContain('test-value');
|
|
1457
|
-
});
|
|
1458
|
-
|
|
1459
|
-
test('header no arg throws', async () => {
|
|
1460
|
-
try {
|
|
1461
|
-
await handleWriteCommand('header', [], bm);
|
|
1462
|
-
expect(true).toBe(false);
|
|
1463
|
-
} catch (err: any) {
|
|
1464
|
-
expect(err.message).toContain('Usage');
|
|
1465
|
-
}
|
|
1466
|
-
});
|
|
1467
|
-
|
|
1468
|
-
test('header no colon throws', async () => {
|
|
1469
|
-
try {
|
|
1470
|
-
await handleWriteCommand('header', ['invalid'], bm);
|
|
1471
|
-
expect(true).toBe(false);
|
|
1472
|
-
} catch (err: any) {
|
|
1473
|
-
expect(err.message).toContain('Usage');
|
|
1474
|
-
}
|
|
1475
|
-
});
|
|
1476
|
-
});
|
|
1477
|
-
|
|
1478
|
-
// ─── PDF command ────────────────────────────────────────────────
|
|
1479
|
-
|
|
1480
|
-
describe('PDF', () => {
|
|
1481
|
-
test('pdf saves file with size', async () => {
|
|
1482
|
-
await handleWriteCommand('goto', [baseUrl + '/basic.html'], bm);
|
|
1483
|
-
const pdfPath = tmpp('browse-test.pdf');
|
|
1484
|
-
const result = await handleMetaCommand('pdf', [pdfPath], bm, async () => {});
|
|
1485
|
-
expect(result).toContain('PDF saved');
|
|
1486
|
-
expect(fs.existsSync(pdfPath)).toBe(true);
|
|
1487
|
-
const stat = fs.statSync(pdfPath);
|
|
1488
|
-
expect(stat.size).toBeGreaterThan(100);
|
|
1489
|
-
fs.unlinkSync(pdfPath);
|
|
1490
|
-
});
|
|
1491
|
-
});
|
|
1492
|
-
|
|
1493
|
-
// ─── Empty page edge cases ──────────────────────────────────────
|
|
1494
|
-
|
|
1495
|
-
describe('Empty page', () => {
|
|
1496
|
-
test('text returns empty on empty page', async () => {
|
|
1497
|
-
await handleWriteCommand('goto', [baseUrl + '/empty.html'], bm);
|
|
1498
|
-
const result = await handleReadCommand('text', [], bm);
|
|
1499
|
-
expect(result).toBe('');
|
|
1500
|
-
});
|
|
1501
|
-
|
|
1502
|
-
test('links returns empty on empty page', async () => {
|
|
1503
|
-
const result = await handleReadCommand('links', [], bm);
|
|
1504
|
-
expect(result).toBe('');
|
|
1505
|
-
});
|
|
1506
|
-
|
|
1507
|
-
test('forms returns empty array on empty page', async () => {
|
|
1508
|
-
const result = await handleReadCommand('forms', [], bm);
|
|
1509
|
-
expect(JSON.parse(result)).toEqual([]);
|
|
1510
|
-
});
|
|
1511
|
-
});
|
|
1512
|
-
|
|
1513
|
-
// ─── Error paths ────────────────────────────────────────────────
|
|
1514
|
-
|
|
1515
|
-
describe('Errors', () => {
|
|
1516
|
-
// Write command errors
|
|
1517
|
-
test('goto with no arg throws', async () => {
|
|
1518
|
-
try {
|
|
1519
|
-
await handleWriteCommand('goto', [], bm);
|
|
1520
|
-
expect(true).toBe(false);
|
|
1521
|
-
} catch (err: any) {
|
|
1522
|
-
expect(err.message).toContain('Usage');
|
|
1523
|
-
}
|
|
1524
|
-
});
|
|
1525
|
-
|
|
1526
|
-
test('click with no arg throws', async () => {
|
|
1527
|
-
try {
|
|
1528
|
-
await handleWriteCommand('click', [], bm);
|
|
1529
|
-
expect(true).toBe(false);
|
|
1530
|
-
} catch (err: any) {
|
|
1531
|
-
expect(err.message).toContain('Usage');
|
|
1532
|
-
}
|
|
1533
|
-
});
|
|
1534
|
-
|
|
1535
|
-
test('fill with no value throws', async () => {
|
|
1536
|
-
try {
|
|
1537
|
-
await handleWriteCommand('fill', ['#input'], bm);
|
|
1538
|
-
expect(true).toBe(false);
|
|
1539
|
-
} catch (err: any) {
|
|
1540
|
-
expect(err.message).toContain('Usage');
|
|
1541
|
-
}
|
|
1542
|
-
});
|
|
1543
|
-
|
|
1544
|
-
test('select with no value throws', async () => {
|
|
1545
|
-
try {
|
|
1546
|
-
await handleWriteCommand('select', ['#sel'], bm);
|
|
1547
|
-
expect(true).toBe(false);
|
|
1548
|
-
} catch (err: any) {
|
|
1549
|
-
expect(err.message).toContain('Usage');
|
|
1550
|
-
}
|
|
1551
|
-
});
|
|
1552
|
-
|
|
1553
|
-
test('hover with no arg throws', async () => {
|
|
1554
|
-
try {
|
|
1555
|
-
await handleWriteCommand('hover', [], bm);
|
|
1556
|
-
expect(true).toBe(false);
|
|
1557
|
-
} catch (err: any) {
|
|
1558
|
-
expect(err.message).toContain('Usage');
|
|
1559
|
-
}
|
|
1560
|
-
});
|
|
1561
|
-
|
|
1562
|
-
test('type with no arg throws', async () => {
|
|
1563
|
-
try {
|
|
1564
|
-
await handleWriteCommand('type', [], bm);
|
|
1565
|
-
expect(true).toBe(false);
|
|
1566
|
-
} catch (err: any) {
|
|
1567
|
-
expect(err.message).toContain('Usage');
|
|
1568
|
-
}
|
|
1569
|
-
});
|
|
1570
|
-
|
|
1571
|
-
test('wait with no arg throws', async () => {
|
|
1572
|
-
try {
|
|
1573
|
-
await handleWriteCommand('wait', [], bm);
|
|
1574
|
-
expect(true).toBe(false);
|
|
1575
|
-
} catch (err: any) {
|
|
1576
|
-
expect(err.message).toContain('Usage');
|
|
1577
|
-
}
|
|
1578
|
-
});
|
|
1579
|
-
|
|
1580
|
-
test('viewport with bad format throws', async () => {
|
|
1581
|
-
try {
|
|
1582
|
-
await handleWriteCommand('viewport', ['badformat'], bm);
|
|
1583
|
-
expect(true).toBe(false);
|
|
1584
|
-
} catch (err: any) {
|
|
1585
|
-
expect(err.message).toContain('Usage');
|
|
1586
|
-
}
|
|
1587
|
-
});
|
|
1588
|
-
|
|
1589
|
-
test('useragent with no arg throws', async () => {
|
|
1590
|
-
try {
|
|
1591
|
-
await handleWriteCommand('useragent', [], bm);
|
|
1592
|
-
expect(true).toBe(false);
|
|
1593
|
-
} catch (err: any) {
|
|
1594
|
-
expect(err.message).toContain('Usage');
|
|
1595
|
-
}
|
|
1596
|
-
});
|
|
1597
|
-
|
|
1598
|
-
// Read command errors
|
|
1599
|
-
test('js with no expression throws', async () => {
|
|
1600
|
-
try {
|
|
1601
|
-
await handleReadCommand('js', [], bm);
|
|
1602
|
-
expect(true).toBe(false);
|
|
1603
|
-
} catch (err: any) {
|
|
1604
|
-
expect(err.message).toContain('Usage');
|
|
1605
|
-
}
|
|
1606
|
-
});
|
|
1607
|
-
|
|
1608
|
-
test('css with missing property throws', async () => {
|
|
1609
|
-
try {
|
|
1610
|
-
await handleReadCommand('css', ['h1'], bm);
|
|
1611
|
-
expect(true).toBe(false);
|
|
1612
|
-
} catch (err: any) {
|
|
1613
|
-
expect(err.message).toContain('Usage');
|
|
1614
|
-
}
|
|
1615
|
-
});
|
|
1616
|
-
|
|
1617
|
-
test('attrs with no selector throws', async () => {
|
|
1618
|
-
try {
|
|
1619
|
-
await handleReadCommand('attrs', [], bm);
|
|
1620
|
-
expect(true).toBe(false);
|
|
1621
|
-
} catch (err: any) {
|
|
1622
|
-
expect(err.message).toContain('Usage');
|
|
1623
|
-
}
|
|
1624
|
-
});
|
|
1625
|
-
|
|
1626
|
-
// Meta command errors
|
|
1627
|
-
test('tab with non-numeric id throws', async () => {
|
|
1628
|
-
try {
|
|
1629
|
-
await handleMetaCommand('tab', ['abc'], bm, async () => {});
|
|
1630
|
-
expect(true).toBe(false);
|
|
1631
|
-
} catch (err: any) {
|
|
1632
|
-
expect(err.message).toContain('Usage');
|
|
1633
|
-
}
|
|
1634
|
-
});
|
|
1635
|
-
|
|
1636
|
-
test('diff with missing urls throws', async () => {
|
|
1637
|
-
try {
|
|
1638
|
-
await handleMetaCommand('diff', [baseUrl + '/basic.html'], bm, async () => {});
|
|
1639
|
-
expect(true).toBe(false);
|
|
1640
|
-
} catch (err: any) {
|
|
1641
|
-
expect(err.message).toContain('Usage');
|
|
1642
|
-
}
|
|
1643
|
-
});
|
|
1644
|
-
|
|
1645
|
-
test('chain with invalid JSON falls back to pipe format', async () => {
|
|
1646
|
-
// Non-JSON input is now treated as pipe-delimited format
|
|
1647
|
-
// 'not json' → [["not", "json"]] → "not" is unknown command → error in result
|
|
1648
|
-
const result = await chainMeta(bm, ['not json']);
|
|
1649
|
-
expect(result).toContain('ERROR');
|
|
1650
|
-
expect(result).toContain('Unknown command: not');
|
|
1651
|
-
});
|
|
1652
|
-
|
|
1653
|
-
test('chain with no arg throws', async () => {
|
|
1654
|
-
try {
|
|
1655
|
-
await chainMeta(bm, []);
|
|
1656
|
-
expect(true).toBe(false);
|
|
1657
|
-
} catch (err: any) {
|
|
1658
|
-
expect(err.message).toContain('Usage');
|
|
1659
|
-
}
|
|
1660
|
-
});
|
|
1661
|
-
|
|
1662
|
-
test('unknown read command throws', async () => {
|
|
1663
|
-
try {
|
|
1664
|
-
await handleReadCommand('bogus' as any, [], bm);
|
|
1665
|
-
expect(true).toBe(false);
|
|
1666
|
-
} catch (err: any) {
|
|
1667
|
-
expect(err.message).toContain('Unknown');
|
|
1668
|
-
}
|
|
1669
|
-
});
|
|
1670
|
-
|
|
1671
|
-
test('unknown write command throws', async () => {
|
|
1672
|
-
try {
|
|
1673
|
-
await handleWriteCommand('bogus' as any, [], bm);
|
|
1674
|
-
expect(true).toBe(false);
|
|
1675
|
-
} catch (err: any) {
|
|
1676
|
-
expect(err.message).toContain('Unknown');
|
|
1677
|
-
}
|
|
1678
|
-
});
|
|
1679
|
-
|
|
1680
|
-
test('unknown meta command throws', async () => {
|
|
1681
|
-
try {
|
|
1682
|
-
await handleMetaCommand('bogus' as any, [], bm, async () => {});
|
|
1683
|
-
expect(true).toBe(false);
|
|
1684
|
-
} catch (err: any) {
|
|
1685
|
-
expect(err.message).toContain('Unknown');
|
|
1686
|
-
}
|
|
1687
|
-
});
|
|
1688
|
-
});
|
|
1689
|
-
|
|
1690
|
-
// ─── Workflow: Navigation + Snapshot + Interaction ───────────────
|
|
1691
|
-
|
|
1692
|
-
describe('Workflows', () => {
|
|
1693
|
-
test('navigation → snapshot → click @ref → verify URL', async () => {
|
|
1694
|
-
await handleWriteCommand('goto', [baseUrl + '/snapshot.html'], bm);
|
|
1695
|
-
const snap = await handleMetaCommand('snapshot', ['-i'], bm, async () => {});
|
|
1696
|
-
// Find a link ref
|
|
1697
|
-
const linkLine = snap.split('\n').find(l => l.includes('[link]'));
|
|
1698
|
-
expect(linkLine).toBeDefined();
|
|
1699
|
-
const refMatch = linkLine!.match(/@(e\d+)/);
|
|
1700
|
-
expect(refMatch).toBeDefined();
|
|
1701
|
-
// Click the link
|
|
1702
|
-
await handleWriteCommand('click', [`@${refMatch![1]}`], bm);
|
|
1703
|
-
// URL should have changed
|
|
1704
|
-
const url = await handleMetaCommand('url', [], bm, async () => {});
|
|
1705
|
-
expect(url).toBeTruthy();
|
|
1706
|
-
});
|
|
1707
|
-
|
|
1708
|
-
test('form: goto → snapshot → fill @ref → click @ref', async () => {
|
|
1709
|
-
await handleWriteCommand('goto', [baseUrl + '/snapshot.html'], bm);
|
|
1710
|
-
const snap = await handleMetaCommand('snapshot', ['-i'], bm, async () => {});
|
|
1711
|
-
// Find textbox and button
|
|
1712
|
-
const textboxLine = snap.split('\n').find(l => l.includes('[textbox]'));
|
|
1713
|
-
const buttonLine = snap.split('\n').find(l => l.includes('[button]') && l.includes('"Submit"'));
|
|
1714
|
-
if (textboxLine && buttonLine) {
|
|
1715
|
-
const textRef = textboxLine.match(/@(e\d+)/)![1];
|
|
1716
|
-
const btnRef = buttonLine.match(/@(e\d+)/)![1];
|
|
1717
|
-
await handleWriteCommand('fill', [`@${textRef}`, 'testuser'], bm);
|
|
1718
|
-
await handleWriteCommand('click', [`@${btnRef}`], bm);
|
|
1719
|
-
}
|
|
1720
|
-
});
|
|
1721
|
-
|
|
1722
|
-
test('tabs: newtab → goto → switch → verify isolation', async () => {
|
|
1723
|
-
await handleWriteCommand('goto', [baseUrl + '/basic.html'], bm);
|
|
1724
|
-
const tabsBefore = bm.getTabCount();
|
|
1725
|
-
await handleMetaCommand('newtab', [baseUrl + '/forms.html'], bm, async () => {});
|
|
1726
|
-
expect(bm.getTabCount()).toBe(tabsBefore + 1);
|
|
1727
|
-
|
|
1728
|
-
const url = await handleMetaCommand('url', [], bm, async () => {});
|
|
1729
|
-
expect(url).toContain('/forms.html');
|
|
1730
|
-
|
|
1731
|
-
// Switch back to previous tab
|
|
1732
|
-
const tabs = await bm.getTabListWithTitles();
|
|
1733
|
-
const prevTab = tabs.find(t => t.url.includes('/basic.html'));
|
|
1734
|
-
if (prevTab) {
|
|
1735
|
-
bm.switchTab(prevTab.id);
|
|
1736
|
-
const url2 = await handleMetaCommand('url', [], bm, async () => {});
|
|
1737
|
-
expect(url2).toContain('/basic.html');
|
|
1738
|
-
}
|
|
1739
|
-
|
|
1740
|
-
// Clean up extra tab
|
|
1741
|
-
const allTabs = await bm.getTabListWithTitles();
|
|
1742
|
-
const formTab = allTabs.find(t => t.url.includes('/forms.html'));
|
|
1743
|
-
if (formTab) await bm.closeTab(formTab.id);
|
|
1744
|
-
});
|
|
1745
|
-
|
|
1746
|
-
test('cookies: set → read → reload → verify persistence', async () => {
|
|
1747
|
-
await handleWriteCommand('goto', [baseUrl + '/basic.html'], bm);
|
|
1748
|
-
await handleWriteCommand('cookie', ['workflow-test=persisted'], bm);
|
|
1749
|
-
await handleWriteCommand('reload', [], bm);
|
|
1750
|
-
const cookies = await handleReadCommand('cookies', [], bm);
|
|
1751
|
-
expect(cookies).toContain('workflow-test');
|
|
1752
|
-
expect(cookies).toContain('persisted');
|
|
1753
|
-
});
|
|
1754
|
-
});
|
|
1755
|
-
|
|
1756
|
-
// ─── Wait load states ──────────────────────────────────────────
|
|
1757
|
-
|
|
1758
|
-
describe('Wait load states', () => {
|
|
1759
|
-
test('wait --networkidle succeeds after page load', async () => {
|
|
1760
|
-
await handleWriteCommand('goto', [baseUrl + '/basic.html'], bm);
|
|
1761
|
-
const result = await handleWriteCommand('wait', ['--networkidle'], bm);
|
|
1762
|
-
expect(result).toBe('Network idle');
|
|
1763
|
-
});
|
|
1764
|
-
|
|
1765
|
-
test('wait --load succeeds', async () => {
|
|
1766
|
-
await handleWriteCommand('goto', [baseUrl + '/basic.html'], bm);
|
|
1767
|
-
const result = await handleWriteCommand('wait', ['--load'], bm);
|
|
1768
|
-
expect(result).toBe('Page loaded');
|
|
1769
|
-
});
|
|
1770
|
-
|
|
1771
|
-
test('wait --domcontentloaded succeeds', async () => {
|
|
1772
|
-
await handleWriteCommand('goto', [baseUrl + '/basic.html'], bm);
|
|
1773
|
-
const result = await handleWriteCommand('wait', ['--domcontentloaded'], bm);
|
|
1774
|
-
expect(result).toBe('DOM content loaded');
|
|
1775
|
-
});
|
|
1776
|
-
|
|
1777
|
-
test('wait --networkidle with custom timeout', async () => {
|
|
1778
|
-
await handleWriteCommand('goto', [baseUrl + '/basic.html'], bm);
|
|
1779
|
-
const result = await handleWriteCommand('wait', ['--networkidle', '5000'], bm);
|
|
1780
|
-
expect(result).toBe('Network idle');
|
|
1781
|
-
});
|
|
1782
|
-
|
|
1783
|
-
test('wait with selector still works', async () => {
|
|
1784
|
-
await handleWriteCommand('goto', [baseUrl + '/basic.html'], bm);
|
|
1785
|
-
const result = await handleWriteCommand('wait', ['#title'], bm);
|
|
1786
|
-
expect(result).toContain('appeared');
|
|
1787
|
-
});
|
|
1788
|
-
});
|
|
1789
|
-
|
|
1790
|
-
// ─── Console --errors ──────────────────────────────────────────
|
|
1791
|
-
|
|
1792
|
-
describe('Console --errors', () => {
|
|
1793
|
-
test('console --errors filters to error and warning only', async () => {
|
|
1794
|
-
// Clear existing entries
|
|
1795
|
-
await handleReadCommand('console', ['--clear'], bm);
|
|
1796
|
-
|
|
1797
|
-
// Add mixed entries
|
|
1798
|
-
addConsoleEntry({ timestamp: Date.now(), level: 'log', text: 'info message' });
|
|
1799
|
-
addConsoleEntry({ timestamp: Date.now(), level: 'warning', text: 'warn message' });
|
|
1800
|
-
addConsoleEntry({ timestamp: Date.now(), level: 'error', text: 'error message' });
|
|
1801
|
-
|
|
1802
|
-
const result = await handleReadCommand('console', ['--errors'], bm);
|
|
1803
|
-
expect(result).toContain('warn message');
|
|
1804
|
-
expect(result).toContain('error message');
|
|
1805
|
-
expect(result).not.toContain('info message');
|
|
1806
|
-
|
|
1807
|
-
// Cleanup
|
|
1808
|
-
consoleBuffer.clear();
|
|
1809
|
-
});
|
|
1810
|
-
|
|
1811
|
-
test('console --errors returns empty message when no errors', async () => {
|
|
1812
|
-
consoleBuffer.clear();
|
|
1813
|
-
addConsoleEntry({ timestamp: Date.now(), level: 'log', text: 'just a log' });
|
|
1814
|
-
|
|
1815
|
-
const result = await handleReadCommand('console', ['--errors'], bm);
|
|
1816
|
-
expect(result).toBe('(no console errors)');
|
|
1817
|
-
|
|
1818
|
-
consoleBuffer.clear();
|
|
1819
|
-
});
|
|
1820
|
-
|
|
1821
|
-
test('console --errors on empty buffer', async () => {
|
|
1822
|
-
consoleBuffer.clear();
|
|
1823
|
-
const result = await handleReadCommand('console', ['--errors'], bm);
|
|
1824
|
-
expect(result).toBe('(no console errors)');
|
|
1825
|
-
});
|
|
1826
|
-
|
|
1827
|
-
test('console without flag still returns all messages', async () => {
|
|
1828
|
-
consoleBuffer.clear();
|
|
1829
|
-
addConsoleEntry({ timestamp: Date.now(), level: 'log', text: 'all messages test' });
|
|
1830
|
-
|
|
1831
|
-
const result = await handleReadCommand('console', [], bm);
|
|
1832
|
-
expect(result).toContain('all messages test');
|
|
1833
|
-
|
|
1834
|
-
consoleBuffer.clear();
|
|
1835
|
-
});
|
|
1836
|
-
});
|
|
1837
|
-
|
|
1838
|
-
// ─── Cookie Import ─────────────────────────────────────────────
|
|
1839
|
-
|
|
1840
|
-
describe('Cookie import', () => {
|
|
1841
|
-
test('cookie-import loads valid JSON cookies', async () => {
|
|
1842
|
-
await handleWriteCommand('goto', [baseUrl + '/basic.html'], bm);
|
|
1843
|
-
const tempFile = tmpp('browse-test-cookies.json');
|
|
1844
|
-
const cookies = [
|
|
1845
|
-
{ name: 'test-cookie', value: 'test-value' },
|
|
1846
|
-
{ name: 'another', value: '123' },
|
|
1847
|
-
];
|
|
1848
|
-
fs.writeFileSync(tempFile, JSON.stringify(cookies));
|
|
1849
|
-
|
|
1850
|
-
const result = await handleWriteCommand('cookie-import', [tempFile], bm);
|
|
1851
|
-
expect(result).toBe(`Loaded 2 cookies from ${tempFile}`);
|
|
1852
|
-
|
|
1853
|
-
// Verify cookies were set
|
|
1854
|
-
const cookieList = await handleReadCommand('cookies', [], bm);
|
|
1855
|
-
expect(cookieList).toContain('test-cookie');
|
|
1856
|
-
expect(cookieList).toContain('test-value');
|
|
1857
|
-
expect(cookieList).toContain('another');
|
|
1858
|
-
|
|
1859
|
-
fs.unlinkSync(tempFile);
|
|
1860
|
-
});
|
|
1861
|
-
|
|
1862
|
-
test('cookie-import auto-fills domain from page URL', async () => {
|
|
1863
|
-
await handleWriteCommand('goto', [baseUrl + '/basic.html'], bm);
|
|
1864
|
-
const tempFile = tmpp('browse-test-cookies-nodomain.json');
|
|
1865
|
-
// Cookies without domain — should auto-fill from page URL
|
|
1866
|
-
const cookies = [{ name: 'autofill-test', value: 'works' }];
|
|
1867
|
-
fs.writeFileSync(tempFile, JSON.stringify(cookies));
|
|
1868
|
-
|
|
1869
|
-
const result = await handleWriteCommand('cookie-import', [tempFile], bm);
|
|
1870
|
-
expect(result).toContain('Loaded 1');
|
|
1871
|
-
|
|
1872
|
-
const cookieList = await handleReadCommand('cookies', [], bm);
|
|
1873
|
-
expect(cookieList).toContain('autofill-test');
|
|
1874
|
-
|
|
1875
|
-
fs.unlinkSync(tempFile);
|
|
1876
|
-
});
|
|
1877
|
-
|
|
1878
|
-
test('cookie-import preserves explicit domain', async () => {
|
|
1879
|
-
await handleWriteCommand('goto', [baseUrl + '/basic.html'], bm);
|
|
1880
|
-
const tempFile = tmpp('browse-test-cookies-domain.json');
|
|
1881
|
-
// Domain must match page hostname (127.0.0.1) — cross-domain cookies are now rejected
|
|
1882
|
-
const cookies = [{ name: 'explicit', value: 'domain', domain: '127.0.0.1', path: '/foo' }];
|
|
1883
|
-
fs.writeFileSync(tempFile, JSON.stringify(cookies));
|
|
1884
|
-
|
|
1885
|
-
const result = await handleWriteCommand('cookie-import', [tempFile], bm);
|
|
1886
|
-
expect(result).toContain('Loaded 1');
|
|
1887
|
-
|
|
1888
|
-
fs.unlinkSync(tempFile);
|
|
1889
|
-
});
|
|
1890
|
-
|
|
1891
|
-
test('cookie-import with empty array succeeds', async () => {
|
|
1892
|
-
await handleWriteCommand('goto', [baseUrl + '/basic.html'], bm);
|
|
1893
|
-
const tempFile = tmpp('browse-test-cookies-empty.json');
|
|
1894
|
-
fs.writeFileSync(tempFile, '[]');
|
|
1895
|
-
|
|
1896
|
-
const result = await handleWriteCommand('cookie-import', [tempFile], bm);
|
|
1897
|
-
expect(result).toBe(`Loaded 0 cookies from ${tempFile}`);
|
|
1898
|
-
|
|
1899
|
-
fs.unlinkSync(tempFile);
|
|
1900
|
-
});
|
|
1901
|
-
|
|
1902
|
-
test('cookie-import throws on file not found', async () => {
|
|
1903
|
-
try {
|
|
1904
|
-
await handleWriteCommand('cookie-import', [tmpp('nonexistent-cookies.json')], bm);
|
|
1905
|
-
expect(true).toBe(false);
|
|
1906
|
-
} catch (err: any) {
|
|
1907
|
-
expect(err.message).toContain('File not found');
|
|
1908
|
-
}
|
|
1909
|
-
});
|
|
1910
|
-
|
|
1911
|
-
test('cookie-import throws on invalid JSON', async () => {
|
|
1912
|
-
const tempFile = tmpp('browse-test-cookies-bad.json');
|
|
1913
|
-
fs.writeFileSync(tempFile, 'not json {{{');
|
|
1914
|
-
|
|
1915
|
-
try {
|
|
1916
|
-
await handleWriteCommand('cookie-import', [tempFile], bm);
|
|
1917
|
-
expect(true).toBe(false);
|
|
1918
|
-
} catch (err: any) {
|
|
1919
|
-
expect(err.message).toContain('Invalid JSON');
|
|
1920
|
-
}
|
|
1921
|
-
|
|
1922
|
-
fs.unlinkSync(tempFile);
|
|
1923
|
-
});
|
|
1924
|
-
|
|
1925
|
-
test('cookie-import throws on non-array JSON', async () => {
|
|
1926
|
-
const tempFile = tmpp('browse-test-cookies-obj.json');
|
|
1927
|
-
fs.writeFileSync(tempFile, '{"name": "not-an-array"}');
|
|
1928
|
-
|
|
1929
|
-
try {
|
|
1930
|
-
await handleWriteCommand('cookie-import', [tempFile], bm);
|
|
1931
|
-
expect(true).toBe(false);
|
|
1932
|
-
} catch (err: any) {
|
|
1933
|
-
expect(err.message).toContain('JSON array');
|
|
1934
|
-
}
|
|
1935
|
-
|
|
1936
|
-
fs.unlinkSync(tempFile);
|
|
1937
|
-
});
|
|
1938
|
-
|
|
1939
|
-
test('cookie-import throws on cookie missing name', async () => {
|
|
1940
|
-
await handleWriteCommand('goto', [baseUrl + '/basic.html'], bm);
|
|
1941
|
-
const tempFile = tmpp('browse-test-cookies-noname.json');
|
|
1942
|
-
fs.writeFileSync(tempFile, JSON.stringify([{ value: 'no-name' }]));
|
|
1943
|
-
|
|
1944
|
-
try {
|
|
1945
|
-
await handleWriteCommand('cookie-import', [tempFile], bm);
|
|
1946
|
-
expect(true).toBe(false);
|
|
1947
|
-
} catch (err: any) {
|
|
1948
|
-
expect(err.message).toContain('name');
|
|
1949
|
-
}
|
|
1950
|
-
|
|
1951
|
-
fs.unlinkSync(tempFile);
|
|
1952
|
-
});
|
|
1953
|
-
|
|
1954
|
-
test('cookie-import no arg throws', async () => {
|
|
1955
|
-
try {
|
|
1956
|
-
await handleWriteCommand('cookie-import', [], bm);
|
|
1957
|
-
expect(true).toBe(false);
|
|
1958
|
-
} catch (err: any) {
|
|
1959
|
-
expect(err.message).toContain('Usage');
|
|
1960
|
-
}
|
|
1961
|
-
});
|
|
1962
|
-
});
|
|
1963
|
-
|
|
1964
|
-
// ─── Security: Redact sensitive values (PR #21) ─────────────────
|
|
1965
|
-
|
|
1966
|
-
describe('Sensitive value redaction', () => {
|
|
1967
|
-
test('type command does not echo typed text', async () => {
|
|
1968
|
-
await handleWriteCommand('goto', [baseUrl + '/basic.html'], bm);
|
|
1969
|
-
const result = await handleWriteCommand('type', ['my-secret-password'], bm);
|
|
1970
|
-
expect(result).not.toContain('my-secret-password');
|
|
1971
|
-
expect(result).toContain('18 characters');
|
|
1972
|
-
});
|
|
1973
|
-
|
|
1974
|
-
test('cookie command redacts value', async () => {
|
|
1975
|
-
await handleWriteCommand('goto', [baseUrl + '/basic.html'], bm);
|
|
1976
|
-
const result = await handleWriteCommand('cookie', ['session=secret123'], bm);
|
|
1977
|
-
expect(result).toContain('session');
|
|
1978
|
-
expect(result).toContain('****');
|
|
1979
|
-
expect(result).not.toContain('secret123');
|
|
1980
|
-
});
|
|
1981
|
-
|
|
1982
|
-
test('header command redacts Authorization value', async () => {
|
|
1983
|
-
const result = await handleWriteCommand('header', ['Authorization:Bearer token-xyz'], bm);
|
|
1984
|
-
expect(result).toContain('Authorization');
|
|
1985
|
-
expect(result).toContain('****');
|
|
1986
|
-
expect(result).not.toContain('token-xyz');
|
|
1987
|
-
});
|
|
1988
|
-
|
|
1989
|
-
test('header command shows non-sensitive values', async () => {
|
|
1990
|
-
const result = await handleWriteCommand('header', ['Content-Type:application/json'], bm);
|
|
1991
|
-
expect(result).toContain('Content-Type');
|
|
1992
|
-
expect(result).toContain('application/json');
|
|
1993
|
-
expect(result).not.toContain('****');
|
|
1994
|
-
});
|
|
1995
|
-
|
|
1996
|
-
test('header command redacts X-API-Key', async () => {
|
|
1997
|
-
const result = await handleWriteCommand('header', ['X-API-Key:sk-12345'], bm);
|
|
1998
|
-
expect(result).toContain('X-API-Key');
|
|
1999
|
-
expect(result).toContain('****');
|
|
2000
|
-
expect(result).not.toContain('sk-12345');
|
|
2001
|
-
});
|
|
2002
|
-
|
|
2003
|
-
test('storage set does not echo value', async () => {
|
|
2004
|
-
await handleWriteCommand('goto', [baseUrl + '/basic.html'], bm);
|
|
2005
|
-
const result = await handleReadCommand('storage', ['set', 'apiKey', 'secret-api-key-value'], bm);
|
|
2006
|
-
expect(result).toContain('apiKey');
|
|
2007
|
-
expect(result).not.toContain('secret-api-key-value');
|
|
2008
|
-
});
|
|
2009
|
-
|
|
2010
|
-
test('forms redacts password field values', async () => {
|
|
2011
|
-
await handleWriteCommand('goto', [baseUrl + '/forms.html'], bm);
|
|
2012
|
-
const formsResult = await handleReadCommand('forms', [], bm);
|
|
2013
|
-
const forms = JSON.parse(formsResult);
|
|
2014
|
-
// Find password fields and verify they're redacted
|
|
2015
|
-
for (const form of forms) {
|
|
2016
|
-
for (const field of form.fields) {
|
|
2017
|
-
if (field.type === 'password') {
|
|
2018
|
-
expect(field.value === undefined || field.value === '[redacted]').toBe(true);
|
|
2019
|
-
}
|
|
2020
|
-
}
|
|
2021
|
-
}
|
|
2022
|
-
});
|
|
2023
|
-
});
|
|
2024
|
-
|
|
2025
|
-
// ─── Security: Path traversal prevention (PR #26) ───────────────
|
|
2026
|
-
|
|
2027
|
-
describe('Path traversal prevention', () => {
|
|
2028
|
-
test('screenshot rejects path outside safe dirs', async () => {
|
|
2029
|
-
await handleWriteCommand('goto', [baseUrl + '/basic.html'], bm);
|
|
2030
|
-
try {
|
|
2031
|
-
await handleMetaCommand('screenshot', ['/etc/evil.png'], bm, () => {});
|
|
2032
|
-
expect(true).toBe(false);
|
|
2033
|
-
} catch (err: any) {
|
|
2034
|
-
expect(err.message).toContain('Path must be within');
|
|
2035
|
-
}
|
|
2036
|
-
});
|
|
2037
|
-
|
|
2038
|
-
test('screenshot allows /tmp path', async () => {
|
|
2039
|
-
await handleWriteCommand('goto', [baseUrl + '/basic.html'], bm);
|
|
2040
|
-
const result = await handleMetaCommand('screenshot', [tmpp('test-safe.png')], bm, () => {});
|
|
2041
|
-
expect(result).toContain('Screenshot saved');
|
|
2042
|
-
try { fs.unlinkSync(tmpp('test-safe.png')); } catch {}
|
|
2043
|
-
});
|
|
2044
|
-
|
|
2045
|
-
test('pdf rejects path outside safe dirs', async () => {
|
|
2046
|
-
await handleWriteCommand('goto', [baseUrl + '/basic.html'], bm);
|
|
2047
|
-
try {
|
|
2048
|
-
await handleMetaCommand('pdf', ['/home/evil.pdf'], bm, () => {});
|
|
2049
|
-
expect(true).toBe(false);
|
|
2050
|
-
} catch (err: any) {
|
|
2051
|
-
expect(err.message).toContain('Path must be within');
|
|
2052
|
-
}
|
|
2053
|
-
});
|
|
2054
|
-
|
|
2055
|
-
test('responsive rejects path outside safe dirs', async () => {
|
|
2056
|
-
await handleWriteCommand('goto', [baseUrl + '/basic.html'], bm);
|
|
2057
|
-
try {
|
|
2058
|
-
await handleMetaCommand('responsive', ['/var/evil'], bm, () => {});
|
|
2059
|
-
expect(true).toBe(false);
|
|
2060
|
-
} catch (err: any) {
|
|
2061
|
-
expect(err.message).toContain('Path must be within');
|
|
2062
|
-
}
|
|
2063
|
-
});
|
|
2064
|
-
|
|
2065
|
-
test('eval rejects path traversal with ..', async () => {
|
|
2066
|
-
try {
|
|
2067
|
-
await handleReadCommand('eval', ['../../etc/passwd'], bm);
|
|
2068
|
-
expect(true).toBe(false);
|
|
2069
|
-
} catch (err: any) {
|
|
2070
|
-
expect(err.message).toContain('Path must be within');
|
|
2071
|
-
}
|
|
2072
|
-
});
|
|
2073
|
-
|
|
2074
|
-
test('eval rejects absolute path outside safe dirs', async () => {
|
|
2075
|
-
try {
|
|
2076
|
-
await handleReadCommand('eval', ['/etc/passwd'], bm);
|
|
2077
|
-
expect(true).toBe(false);
|
|
2078
|
-
} catch (err: any) {
|
|
2079
|
-
expect(err.message).toContain('Path must be within');
|
|
2080
|
-
}
|
|
2081
|
-
});
|
|
2082
|
-
|
|
2083
|
-
test('eval allows /tmp path', async () => {
|
|
2084
|
-
const tmpFile = tmpp('test-eval-safe.js');
|
|
2085
|
-
fs.writeFileSync(tmpFile, 'document.title');
|
|
2086
|
-
try {
|
|
2087
|
-
const result = await handleReadCommand('eval', [tmpFile], bm);
|
|
2088
|
-
expect(typeof result).toBe('string');
|
|
2089
|
-
} finally {
|
|
2090
|
-
try { fs.unlinkSync(tmpFile); } catch {}
|
|
2091
|
-
}
|
|
2092
|
-
});
|
|
2093
|
-
|
|
2094
|
-
test('screenshot rejects /tmpevil prefix collision', async () => {
|
|
2095
|
-
await handleWriteCommand('goto', [baseUrl + '/basic.html'], bm);
|
|
2096
|
-
try {
|
|
2097
|
-
await handleMetaCommand('screenshot', ['/tmpevil/steal.png'], bm, () => {});
|
|
2098
|
-
expect(true).toBe(false);
|
|
2099
|
-
} catch (err: any) {
|
|
2100
|
-
expect(err.message).toContain('Path must be within');
|
|
2101
|
-
}
|
|
2102
|
-
});
|
|
2103
|
-
|
|
2104
|
-
test('cookie-import rejects path traversal', async () => {
|
|
2105
|
-
try {
|
|
2106
|
-
await handleWriteCommand('cookie-import', ['../../etc/shadow'], bm);
|
|
2107
|
-
expect(true).toBe(false);
|
|
2108
|
-
} catch (err: any) {
|
|
2109
|
-
// Traversal blocked by safe-directory check (#707) or explicit .. check
|
|
2110
|
-
expect(err.message).toMatch(/Path must be within|Path traversal/);
|
|
2111
|
-
}
|
|
2112
|
-
});
|
|
2113
|
-
|
|
2114
|
-
test('cookie-import rejects absolute path outside safe dirs', async () => {
|
|
2115
|
-
try {
|
|
2116
|
-
await handleWriteCommand('cookie-import', ['/etc/passwd'], bm);
|
|
2117
|
-
expect(true).toBe(false);
|
|
2118
|
-
} catch (err: any) {
|
|
2119
|
-
expect(err.message).toContain('Path must be within');
|
|
2120
|
-
}
|
|
2121
|
-
});
|
|
2122
|
-
|
|
2123
|
-
test('snapshot -a -o rejects path outside safe dirs', async () => {
|
|
2124
|
-
await handleWriteCommand('goto', [baseUrl + '/basic.html'], bm);
|
|
2125
|
-
// First get a snapshot so refs exist
|
|
2126
|
-
await handleMetaCommand('snapshot', ['-i'], bm, () => {});
|
|
2127
|
-
try {
|
|
2128
|
-
await handleMetaCommand('snapshot', ['-a', '-o', '/etc/evil.png'], bm, () => {});
|
|
2129
|
-
expect(true).toBe(false);
|
|
2130
|
-
} catch (err: any) {
|
|
2131
|
-
expect(err.message).toContain('Path must be within');
|
|
2132
|
-
}
|
|
2133
|
-
});
|
|
2134
|
-
});
|
|
2135
|
-
|
|
2136
|
-
// ─── Chain command: cookie-import in chain ──────────────────────
|
|
2137
|
-
|
|
2138
|
-
describe('Chain with cookie-import', () => {
|
|
2139
|
-
test('cookie-import works inside chain', async () => {
|
|
2140
|
-
await handleWriteCommand('goto', [baseUrl + '/basic.html'], bm);
|
|
2141
|
-
const tmpCookies = tmpp('test-chain-cookies.json');
|
|
2142
|
-
fs.writeFileSync(tmpCookies, JSON.stringify([
|
|
2143
|
-
{ name: 'chain_test', value: 'chain_value', domain: '127.0.0.1', path: '/' }
|
|
2144
|
-
]));
|
|
2145
|
-
try {
|
|
2146
|
-
const commands = JSON.stringify([
|
|
2147
|
-
['cookie-import', tmpCookies],
|
|
2148
|
-
]);
|
|
2149
|
-
const result = await chainMeta(bm, [commands]);
|
|
2150
|
-
expect(result).toContain('[cookie-import]');
|
|
2151
|
-
expect(result).toContain('Loaded 1 cookie');
|
|
2152
|
-
} finally {
|
|
2153
|
-
try { fs.unlinkSync(tmpCookies); } catch {}
|
|
2154
|
-
}
|
|
2155
|
-
});
|
|
2156
|
-
});
|
|
2157
|
-
|
|
2158
|
-
// ─── Network Idle Detection ─────────────────────────────────────
|
|
2159
|
-
|
|
2160
|
-
describe('Network idle', () => {
|
|
2161
|
-
test('click on fetch button waits for XHR to complete', async () => {
|
|
2162
|
-
await handleWriteCommand('goto', [baseUrl + '/network-idle.html'], bm);
|
|
2163
|
-
// Click the button that triggers a fetch → networkidle waits for it
|
|
2164
|
-
await handleWriteCommand('click', ['#fetch-btn'], bm);
|
|
2165
|
-
// The DOM should be updated by the time click returns
|
|
2166
|
-
const result = await handleReadCommand('js', ['document.getElementById("result").textContent'], bm);
|
|
2167
|
-
expect(result).toContain('Data loaded');
|
|
2168
|
-
});
|
|
2169
|
-
|
|
2170
|
-
test('click on static button has no latency penalty', async () => {
|
|
2171
|
-
await handleWriteCommand('goto', [baseUrl + '/network-idle.html'], bm);
|
|
2172
|
-
const start = Date.now();
|
|
2173
|
-
await handleWriteCommand('click', ['#static-btn'], bm);
|
|
2174
|
-
const elapsed = Date.now() - start;
|
|
2175
|
-
// Static click should complete well under 2s (the networkidle timeout)
|
|
2176
|
-
// networkidle resolves immediately when no requests are in flight
|
|
2177
|
-
expect(elapsed).toBeLessThan(1500);
|
|
2178
|
-
const result = await handleReadCommand('js', ['document.getElementById("static-result").textContent'], bm);
|
|
2179
|
-
expect(result).toBe('Static action done');
|
|
2180
|
-
});
|
|
2181
|
-
|
|
2182
|
-
test('fill triggers networkidle wait', async () => {
|
|
2183
|
-
await handleWriteCommand('goto', [baseUrl + '/forms.html'], bm);
|
|
2184
|
-
// fill should complete without error (networkidle resolves immediately on static page)
|
|
2185
|
-
const result = await handleWriteCommand('fill', ['#email', 'idle@test.com'], bm);
|
|
2186
|
-
expect(result).toContain('Filled');
|
|
2187
|
-
});
|
|
2188
|
-
});
|
|
2189
|
-
|
|
2190
|
-
// ─── Chain Pipe Format ──────────────────────────────────────────
|
|
2191
|
-
|
|
2192
|
-
describe('Chain pipe format', () => {
|
|
2193
|
-
test('pipe-delimited commands work', async () => {
|
|
2194
|
-
const result = await chainMeta(bm, [`goto ${baseUrl}/basic.html | js document.title`]);
|
|
2195
|
-
expect(result).toContain('[goto]');
|
|
2196
|
-
expect(result).toContain('[js]');
|
|
2197
|
-
expect(result).toContain('Test Page - Basic');
|
|
2198
|
-
});
|
|
2199
|
-
|
|
2200
|
-
test('pipe format with quoted args', async () => {
|
|
2201
|
-
const result = await chainMeta(bm, [`goto ${baseUrl}/forms.html | fill #email "pipe@test.com"`]);
|
|
2202
|
-
expect(result).toContain('[fill]');
|
|
2203
|
-
expect(result).toContain('Filled');
|
|
2204
|
-
// Verify the fill actually worked
|
|
2205
|
-
const val = await handleReadCommand('js', ['document.querySelector("#email").value'], bm);
|
|
2206
|
-
expect(val).toBe('pipe@test.com');
|
|
2207
|
-
});
|
|
2208
|
-
|
|
2209
|
-
test('JSON format still works', async () => {
|
|
2210
|
-
const commands = JSON.stringify([
|
|
2211
|
-
['goto', baseUrl + '/basic.html'],
|
|
2212
|
-
['js', 'document.title'],
|
|
2213
|
-
]);
|
|
2214
|
-
const result = await chainMeta(bm, [commands]);
|
|
2215
|
-
expect(result).toContain('[goto]');
|
|
2216
|
-
expect(result).toContain('Test Page - Basic');
|
|
2217
|
-
});
|
|
2218
|
-
|
|
2219
|
-
test('pipe format with unknown command includes error', async () => {
|
|
2220
|
-
const result = await chainMeta(bm, ['bogus command']);
|
|
2221
|
-
expect(result).toContain('ERROR');
|
|
2222
|
-
expect(result).toContain('Unknown command: bogus');
|
|
2223
|
-
});
|
|
2224
|
-
});
|
|
2225
|
-
|
|
2226
|
-
// ─── State Persistence ──────────────────────────────────────────
|
|
2227
|
-
|
|
2228
|
-
describe('State persistence', () => {
|
|
2229
|
-
test('state save and load round-trip', async () => {
|
|
2230
|
-
await handleWriteCommand('goto', [baseUrl + '/basic.html'], bm);
|
|
2231
|
-
// Set a cookie so we can verify it persists
|
|
2232
|
-
await handleWriteCommand('cookie', ['state_test=hello'], bm);
|
|
2233
|
-
|
|
2234
|
-
// Save state
|
|
2235
|
-
const saveResult = await handleMetaCommand('state', ['save', 'test-roundtrip'], bm, async () => {});
|
|
2236
|
-
expect(saveResult).toContain('State saved');
|
|
2237
|
-
expect(saveResult).toContain('Cookies stored in plaintext');
|
|
2238
|
-
|
|
2239
|
-
// Navigate away
|
|
2240
|
-
await handleWriteCommand('goto', [baseUrl + '/forms.html'], bm);
|
|
2241
|
-
|
|
2242
|
-
// Load state — should restore to basic.html with cookie
|
|
2243
|
-
const loadResult = await handleMetaCommand('state', ['load', 'test-roundtrip'], bm, async () => {});
|
|
2244
|
-
expect(loadResult).toContain('State loaded');
|
|
2245
|
-
|
|
2246
|
-
// Verify we're back on basic.html
|
|
2247
|
-
const url = await handleReadCommand('js', ['location.pathname'], bm);
|
|
2248
|
-
expect(url).toContain('basic.html');
|
|
2249
|
-
|
|
2250
|
-
// Clean up
|
|
2251
|
-
try {
|
|
2252
|
-
const { resolveConfig } = await import('../src/config');
|
|
2253
|
-
const config = resolveConfig();
|
|
2254
|
-
fs.unlinkSync(`${config.stateDir}/browse-states/test-roundtrip.json`);
|
|
2255
|
-
} catch {}
|
|
2256
|
-
});
|
|
2257
|
-
|
|
2258
|
-
test('state save rejects invalid names', async () => {
|
|
2259
|
-
try {
|
|
2260
|
-
await handleMetaCommand('state', ['save', '../../evil'], bm, async () => {});
|
|
2261
|
-
expect(true).toBe(false);
|
|
2262
|
-
} catch (err: any) {
|
|
2263
|
-
expect(err.message).toContain('alphanumeric');
|
|
2264
|
-
}
|
|
2265
|
-
});
|
|
2266
|
-
|
|
2267
|
-
test('state save accepts valid names', async () => {
|
|
2268
|
-
const result = await handleMetaCommand('state', ['save', 'my-state_1'], bm, async () => {});
|
|
2269
|
-
expect(result).toContain('State saved');
|
|
2270
|
-
// Clean up
|
|
2271
|
-
try {
|
|
2272
|
-
const { resolveConfig } = await import('../src/config');
|
|
2273
|
-
const config = resolveConfig();
|
|
2274
|
-
fs.unlinkSync(`${config.stateDir}/browse-states/my-state_1.json`);
|
|
2275
|
-
} catch {}
|
|
2276
|
-
});
|
|
2277
|
-
|
|
2278
|
-
test('state load rejects missing state', async () => {
|
|
2279
|
-
try {
|
|
2280
|
-
await handleMetaCommand('state', ['load', 'nonexistent-state-xyz'], bm, async () => {});
|
|
2281
|
-
expect(true).toBe(false);
|
|
2282
|
-
} catch (err: any) {
|
|
2283
|
-
expect(err.message).toContain('State not found');
|
|
2284
|
-
}
|
|
2285
|
-
});
|
|
2286
|
-
|
|
2287
|
-
test('state requires action and name', async () => {
|
|
2288
|
-
try {
|
|
2289
|
-
await handleMetaCommand('state', [], bm, async () => {});
|
|
2290
|
-
expect(true).toBe(false);
|
|
2291
|
-
} catch (err: any) {
|
|
2292
|
-
expect(err.message).toContain('Usage');
|
|
2293
|
-
}
|
|
2294
|
-
});
|
|
2295
|
-
});
|
|
2296
|
-
|
|
2297
|
-
// ─── Frame (Iframe Support) ─────────────────────────────────────
|
|
2298
|
-
|
|
2299
|
-
describe('Frame', () => {
|
|
2300
|
-
test('frame switch to iframe and back', async () => {
|
|
2301
|
-
await handleWriteCommand('goto', [baseUrl + '/iframe.html'], bm);
|
|
2302
|
-
|
|
2303
|
-
// Verify we're on the main page
|
|
2304
|
-
const mainTitle = await handleReadCommand('js', ['document.getElementById("main-title").textContent'], bm);
|
|
2305
|
-
expect(mainTitle).toBe('Main Page');
|
|
2306
|
-
|
|
2307
|
-
// Switch to iframe by CSS selector
|
|
2308
|
-
const switchResult = await handleMetaCommand('frame', ['#test-frame'], bm, async () => {});
|
|
2309
|
-
expect(switchResult).toContain('Switched to frame');
|
|
2310
|
-
|
|
2311
|
-
// Verify we can read iframe content
|
|
2312
|
-
const frameTitle = await handleReadCommand('js', ['document.getElementById("frame-title").textContent'], bm);
|
|
2313
|
-
expect(frameTitle).toBe('Inside Frame');
|
|
2314
|
-
|
|
2315
|
-
// Switch back to main
|
|
2316
|
-
const mainResult = await handleMetaCommand('frame', ['main'], bm, async () => {});
|
|
2317
|
-
expect(mainResult).toBe('Switched to main frame');
|
|
2318
|
-
|
|
2319
|
-
// Verify we're back on the main page
|
|
2320
|
-
const mainTitleAgain = await handleReadCommand('js', ['document.getElementById("main-title").textContent'], bm);
|
|
2321
|
-
expect(mainTitleAgain).toBe('Main Page');
|
|
2322
|
-
});
|
|
2323
|
-
|
|
2324
|
-
test('snapshot shows frame context header', async () => {
|
|
2325
|
-
await handleWriteCommand('goto', [baseUrl + '/iframe.html'], bm);
|
|
2326
|
-
await handleMetaCommand('frame', ['#test-frame'], bm, async () => {});
|
|
2327
|
-
|
|
2328
|
-
const snap = await handleMetaCommand('snapshot', ['-i'], bm, async () => {});
|
|
2329
|
-
expect(snap).toContain('[Context: iframe');
|
|
2330
|
-
|
|
2331
|
-
// Clean up — return to main
|
|
2332
|
-
await handleMetaCommand('frame', ['main'], bm, async () => {});
|
|
2333
|
-
});
|
|
2334
|
-
|
|
2335
|
-
test('goto throws error when in frame context', async () => {
|
|
2336
|
-
await handleWriteCommand('goto', [baseUrl + '/iframe.html'], bm);
|
|
2337
|
-
await handleMetaCommand('frame', ['#test-frame'], bm, async () => {});
|
|
2338
|
-
|
|
2339
|
-
try {
|
|
2340
|
-
await handleWriteCommand('goto', ['https://example.com'], bm);
|
|
2341
|
-
expect(true).toBe(false);
|
|
2342
|
-
} catch (err: any) {
|
|
2343
|
-
expect(err.message).toContain('Cannot use goto inside a frame');
|
|
2344
|
-
}
|
|
2345
|
-
|
|
2346
|
-
await handleMetaCommand('frame', ['main'], bm, async () => {});
|
|
2347
|
-
});
|
|
2348
|
-
|
|
2349
|
-
test('frame requires argument', async () => {
|
|
2350
|
-
try {
|
|
2351
|
-
await handleMetaCommand('frame', [], bm, async () => {});
|
|
2352
|
-
expect(true).toBe(false);
|
|
2353
|
-
} catch (err: any) {
|
|
2354
|
-
expect(err.message).toContain('Usage');
|
|
2355
|
-
}
|
|
2356
|
-
});
|
|
2357
|
-
|
|
2358
|
-
test('fill works inside iframe', async () => {
|
|
2359
|
-
await handleWriteCommand('goto', [baseUrl + '/iframe.html'], bm);
|
|
2360
|
-
await handleMetaCommand('frame', ['#test-frame'], bm, async () => {});
|
|
2361
|
-
|
|
2362
|
-
const result = await handleWriteCommand('fill', ['#frame-input', 'hello from frame'], bm);
|
|
2363
|
-
expect(result).toContain('Filled');
|
|
2364
|
-
|
|
2365
|
-
const value = await handleReadCommand('js', ['document.getElementById("frame-input").value'], bm);
|
|
2366
|
-
expect(value).toBe('hello from frame');
|
|
2367
|
-
|
|
2368
|
-
await handleMetaCommand('frame', ['main'], bm, async () => {});
|
|
2369
|
-
});
|
|
2370
|
-
});
|
|
2371
|
-
|
|
2372
|
-
// ─── load-html ─────────────────────────────────────────────────
|
|
2373
|
-
|
|
2374
|
-
describe('load-html', () => {
|
|
2375
|
-
const tmpDir = '/tmp';
|
|
2376
|
-
const fixturePath = path.join(tmpDir, `browse-test-loadhtml-${Date.now()}.html`);
|
|
2377
|
-
const fragmentPath = path.join(tmpDir, `browse-test-fragment-${Date.now()}.html`);
|
|
2378
|
-
|
|
2379
|
-
beforeAll(() => {
|
|
2380
|
-
fs.writeFileSync(fixturePath, '<html><body><h1 id="loaded">loaded by load-html</h1></body></html>');
|
|
2381
|
-
fs.writeFileSync(fragmentPath, '<div class="fragment" style="width:100px;height:50px">fragment</div>');
|
|
2382
|
-
});
|
|
2383
|
-
|
|
2384
|
-
afterAll(() => {
|
|
2385
|
-
try { fs.unlinkSync(fixturePath); } catch {}
|
|
2386
|
-
try { fs.unlinkSync(fragmentPath); } catch {}
|
|
2387
|
-
});
|
|
2388
|
-
|
|
2389
|
-
test('load-html loads HTML file into page', async () => {
|
|
2390
|
-
const result = await handleWriteCommand('load-html', [fixturePath], bm);
|
|
2391
|
-
expect(result).toContain('Loaded HTML:');
|
|
2392
|
-
expect(result).toContain(fixturePath);
|
|
2393
|
-
const text = await handleReadCommand('text', [], bm);
|
|
2394
|
-
expect(text).toContain('loaded by load-html');
|
|
2395
|
-
});
|
|
2396
|
-
|
|
2397
|
-
test('load-html accepts bare HTML fragments (no doctype)', async () => {
|
|
2398
|
-
const result = await handleWriteCommand('load-html', [fragmentPath], bm);
|
|
2399
|
-
expect(result).toContain('Loaded HTML:');
|
|
2400
|
-
const html = await handleReadCommand('html', [], bm);
|
|
2401
|
-
expect(html).toContain('fragment');
|
|
2402
|
-
});
|
|
2403
|
-
|
|
2404
|
-
test('load-html rejects missing file arg', async () => {
|
|
2405
|
-
try {
|
|
2406
|
-
await handleWriteCommand('load-html', [], bm);
|
|
2407
|
-
expect(true).toBe(false);
|
|
2408
|
-
} catch (err: any) {
|
|
2409
|
-
expect(err.message).toMatch(/Usage: browse load-html/);
|
|
2410
|
-
}
|
|
2411
|
-
});
|
|
2412
|
-
|
|
2413
|
-
test('load-html rejects non-.html extension', async () => {
|
|
2414
|
-
const txtPath = path.join(tmpDir, `load-html-test-${Date.now()}.txt`);
|
|
2415
|
-
fs.writeFileSync(txtPath, '<html></html>');
|
|
2416
|
-
try {
|
|
2417
|
-
await handleWriteCommand('load-html', [txtPath], bm);
|
|
2418
|
-
expect(true).toBe(false);
|
|
2419
|
-
} catch (err: any) {
|
|
2420
|
-
expect(err.message).toMatch(/does not appear to be HTML/);
|
|
2421
|
-
} finally {
|
|
2422
|
-
try { fs.unlinkSync(txtPath); } catch {}
|
|
2423
|
-
}
|
|
2424
|
-
});
|
|
2425
|
-
|
|
2426
|
-
test('load-html rejects .svg files', async () => {
|
|
2427
|
-
const svgPath = path.join(tmpDir, `load-html-test-${Date.now()}.svg`);
|
|
2428
|
-
fs.writeFileSync(svgPath, '<svg xmlns="http://www.w3.org/2000/svg"><text>hi</text></svg>');
|
|
2429
|
-
try {
|
|
2430
|
-
await handleWriteCommand('load-html', [svgPath], bm);
|
|
2431
|
-
expect(true).toBe(false);
|
|
2432
|
-
} catch (err: any) {
|
|
2433
|
-
expect(err.message).toMatch(/does not appear to be HTML/);
|
|
2434
|
-
} finally {
|
|
2435
|
-
try { fs.unlinkSync(svgPath); } catch {}
|
|
2436
|
-
}
|
|
2437
|
-
});
|
|
2438
|
-
|
|
2439
|
-
test('load-html rejects file outside safe dirs', async () => {
|
|
2440
|
-
try {
|
|
2441
|
-
await handleWriteCommand('load-html', ['/etc/passwd.html'], bm);
|
|
2442
|
-
expect(true).toBe(false);
|
|
2443
|
-
} catch (err: any) {
|
|
2444
|
-
expect(err.message).toMatch(/must be under|not found|security policy/);
|
|
2445
|
-
}
|
|
2446
|
-
});
|
|
2447
|
-
|
|
2448
|
-
test('load-html rejects missing file with actionable error', async () => {
|
|
2449
|
-
try {
|
|
2450
|
-
await handleWriteCommand('load-html', [path.join(tmpDir, 'does-not-exist.html')], bm);
|
|
2451
|
-
expect(true).toBe(false);
|
|
2452
|
-
} catch (err: any) {
|
|
2453
|
-
expect(err.message).toMatch(/not found|security policy/);
|
|
2454
|
-
}
|
|
2455
|
-
});
|
|
2456
|
-
|
|
2457
|
-
test('load-html rejects directory target', async () => {
|
|
2458
|
-
try {
|
|
2459
|
-
await handleWriteCommand('load-html', [path.join(tmpDir, 'browse-test-notafile.html') + '/'], bm);
|
|
2460
|
-
expect(true).toBe(false);
|
|
2461
|
-
} catch (err: any) {
|
|
2462
|
-
// Either "not found" or "is a directory" — both valid rejections
|
|
2463
|
-
expect(err.message).toMatch(/not found|directory|not a regular file|security policy/);
|
|
2464
|
-
}
|
|
2465
|
-
});
|
|
2466
|
-
|
|
2467
|
-
test('load-html rejects binary content disguised as .html', async () => {
|
|
2468
|
-
const binPath = path.join(tmpDir, `load-html-binary-${Date.now()}.html`);
|
|
2469
|
-
// PNG magic bytes: 0x89 0x50 0x4E 0x47
|
|
2470
|
-
fs.writeFileSync(binPath, Buffer.from([0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]));
|
|
2471
|
-
try {
|
|
2472
|
-
await handleWriteCommand('load-html', [binPath], bm);
|
|
2473
|
-
expect(true).toBe(false);
|
|
2474
|
-
} catch (err: any) {
|
|
2475
|
-
expect(err.message).toMatch(/does not look like HTML/);
|
|
2476
|
-
} finally {
|
|
2477
|
-
try { fs.unlinkSync(binPath); } catch {}
|
|
2478
|
-
}
|
|
2479
|
-
});
|
|
2480
|
-
|
|
2481
|
-
test('load-html strips UTF-8 BOM before magic-byte check', async () => {
|
|
2482
|
-
const bomPath = path.join(tmpDir, `load-html-bom-${Date.now()}.html`);
|
|
2483
|
-
const bomBytes = Buffer.from([0xEF, 0xBB, 0xBF]);
|
|
2484
|
-
fs.writeFileSync(bomPath, Buffer.concat([bomBytes, Buffer.from('<html><body>bom ok</body></html>')]));
|
|
2485
|
-
try {
|
|
2486
|
-
const result = await handleWriteCommand('load-html', [bomPath], bm);
|
|
2487
|
-
expect(result).toContain('Loaded HTML:');
|
|
2488
|
-
} finally {
|
|
2489
|
-
try { fs.unlinkSync(bomPath); } catch {}
|
|
2490
|
-
}
|
|
2491
|
-
});
|
|
2492
|
-
|
|
2493
|
-
test('load-html --wait-until networkidle exercises non-default branch', async () => {
|
|
2494
|
-
const result = await handleWriteCommand('load-html', [fixturePath, '--wait-until', 'networkidle'], bm);
|
|
2495
|
-
expect(result).toContain('Loaded HTML:');
|
|
2496
|
-
});
|
|
2497
|
-
|
|
2498
|
-
test('load-html rejects invalid --wait-until value', async () => {
|
|
2499
|
-
try {
|
|
2500
|
-
await handleWriteCommand('load-html', [fixturePath, '--wait-until', 'bogus'], bm);
|
|
2501
|
-
expect(true).toBe(false);
|
|
2502
|
-
} catch (err: any) {
|
|
2503
|
-
expect(err.message).toMatch(/Invalid --wait-until/);
|
|
2504
|
-
}
|
|
2505
|
-
});
|
|
2506
|
-
|
|
2507
|
-
test('load-html rejects unknown flag', async () => {
|
|
2508
|
-
try {
|
|
2509
|
-
await handleWriteCommand('load-html', [fixturePath, '--bogus'], bm);
|
|
2510
|
-
expect(true).toBe(false);
|
|
2511
|
-
} catch (err: any) {
|
|
2512
|
-
expect(err.message).toMatch(/Unknown flag/);
|
|
2513
|
-
}
|
|
2514
|
-
});
|
|
2515
|
-
});
|
|
2516
|
-
|
|
2517
|
-
// ─── screenshot --selector ─────────────────────────────────────
|
|
2518
|
-
|
|
2519
|
-
describe('screenshot --selector', () => {
|
|
2520
|
-
test('--selector flag with output path captures element', async () => {
|
|
2521
|
-
await handleWriteCommand('goto', [baseUrl + '/basic.html'], bm);
|
|
2522
|
-
const p = tmpp(`browse-test-selector-${Date.now()}.png`);
|
|
2523
|
-
const result = await handleMetaCommand('screenshot', ['--selector', '#title', p], bm, async () => {});
|
|
2524
|
-
expect(result).toContain('Screenshot saved (element)');
|
|
2525
|
-
expect(fs.existsSync(p)).toBe(true);
|
|
2526
|
-
fs.unlinkSync(p);
|
|
2527
|
-
});
|
|
2528
|
-
|
|
2529
|
-
test('--selector conflicts with positional selector', async () => {
|
|
2530
|
-
await handleWriteCommand('goto', [baseUrl + '/basic.html'], bm);
|
|
2531
|
-
try {
|
|
2532
|
-
await handleMetaCommand('screenshot', ['--selector', '#title', '.other'], bm, async () => {});
|
|
2533
|
-
expect(true).toBe(false);
|
|
2534
|
-
} catch (err: any) {
|
|
2535
|
-
expect(err.message).toMatch(/conflicts with positional selector/);
|
|
2536
|
-
}
|
|
2537
|
-
});
|
|
2538
|
-
|
|
2539
|
-
test('--selector conflicts with --clip', async () => {
|
|
2540
|
-
await handleWriteCommand('goto', [baseUrl + '/basic.html'], bm);
|
|
2541
|
-
try {
|
|
2542
|
-
await handleMetaCommand('screenshot', ['--selector', '#title', '--clip', '0,0,100,100'], bm, async () => {});
|
|
2543
|
-
expect(true).toBe(false);
|
|
2544
|
-
} catch (err: any) {
|
|
2545
|
-
expect(err.message).toMatch(/Cannot use --clip with a selector/);
|
|
2546
|
-
}
|
|
2547
|
-
});
|
|
2548
|
-
|
|
2549
|
-
test('--selector with --base64 returns element base64', async () => {
|
|
2550
|
-
await handleWriteCommand('goto', [baseUrl + '/basic.html'], bm);
|
|
2551
|
-
const result = await handleMetaCommand('screenshot', ['--selector', '#title', '--base64'], bm, async () => {});
|
|
2552
|
-
expect(result).toMatch(/^data:image\/png;base64,/);
|
|
2553
|
-
});
|
|
2554
|
-
|
|
2555
|
-
test('--selector missing value throws', async () => {
|
|
2556
|
-
await handleWriteCommand('goto', [baseUrl + '/basic.html'], bm);
|
|
2557
|
-
try {
|
|
2558
|
-
await handleMetaCommand('screenshot', ['--selector'], bm, async () => {});
|
|
2559
|
-
expect(true).toBe(false);
|
|
2560
|
-
} catch (err: any) {
|
|
2561
|
-
expect(err.message).toMatch(/Usage: screenshot --selector/);
|
|
2562
|
-
}
|
|
2563
|
-
});
|
|
2564
|
-
});
|
|
2565
|
-
|
|
2566
|
-
// ─── viewport --scale ───────────────────────────────────────────
|
|
2567
|
-
|
|
2568
|
-
describe('viewport --scale', () => {
|
|
2569
|
-
test('viewport WxH --scale 2 produces 2x dimension screenshot', async () => {
|
|
2570
|
-
const tmpFix = path.join('/tmp', `scale-${Date.now()}.html`);
|
|
2571
|
-
fs.writeFileSync(tmpFix, '<div id="box" style="width:100px;height:50px;background:#f00"></div>');
|
|
2572
|
-
try {
|
|
2573
|
-
await handleWriteCommand('viewport', ['200x200', '--scale', '2'], bm);
|
|
2574
|
-
await handleWriteCommand('load-html', [tmpFix], bm);
|
|
2575
|
-
const p = tmpp(`scale-${Date.now()}.png`);
|
|
2576
|
-
await handleMetaCommand('screenshot', ['--selector', '#box', p], bm, async () => {});
|
|
2577
|
-
// Parse PNG IHDR (bytes 16-23 are width/height big-endian u32)
|
|
2578
|
-
const buf = fs.readFileSync(p);
|
|
2579
|
-
const w = buf.readUInt32BE(16);
|
|
2580
|
-
const h = buf.readUInt32BE(20);
|
|
2581
|
-
// Box is 100x50 at 2x = 200x100
|
|
2582
|
-
expect(w).toBe(200);
|
|
2583
|
-
expect(h).toBe(100);
|
|
2584
|
-
fs.unlinkSync(p);
|
|
2585
|
-
// Reset scale for other tests
|
|
2586
|
-
await handleWriteCommand('viewport', ['1280x720', '--scale', '1'], bm);
|
|
2587
|
-
} finally {
|
|
2588
|
-
try { fs.unlinkSync(tmpFix); } catch {}
|
|
2589
|
-
}
|
|
2590
|
-
});
|
|
2591
|
-
|
|
2592
|
-
test('viewport --scale without WxH keeps current size', async () => {
|
|
2593
|
-
await handleWriteCommand('viewport', ['800x600'], bm);
|
|
2594
|
-
const result = await handleWriteCommand('viewport', ['--scale', '2'], bm);
|
|
2595
|
-
expect(result).toContain('800x600');
|
|
2596
|
-
expect(result).toContain('2x');
|
|
2597
|
-
expect(bm.getDeviceScaleFactor()).toBe(2);
|
|
2598
|
-
await handleWriteCommand('viewport', ['1280x720', '--scale', '1'], bm);
|
|
2599
|
-
});
|
|
2600
|
-
|
|
2601
|
-
test('--scale non-finite (NaN) throws', async () => {
|
|
2602
|
-
try {
|
|
2603
|
-
await handleWriteCommand('viewport', ['100x100', '--scale', 'abc'], bm);
|
|
2604
|
-
expect(true).toBe(false);
|
|
2605
|
-
} catch (err: any) {
|
|
2606
|
-
expect(err.message).toMatch(/not a finite number/);
|
|
2607
|
-
}
|
|
2608
|
-
});
|
|
2609
|
-
|
|
2610
|
-
test('--scale out of range throws', async () => {
|
|
2611
|
-
try {
|
|
2612
|
-
await handleWriteCommand('viewport', ['100x100', '--scale', '4'], bm);
|
|
2613
|
-
expect(true).toBe(false);
|
|
2614
|
-
} catch (err: any) {
|
|
2615
|
-
expect(err.message).toMatch(/between 1 and 3/);
|
|
2616
|
-
}
|
|
2617
|
-
try {
|
|
2618
|
-
await handleWriteCommand('viewport', ['100x100', '--scale', '0.5'], bm);
|
|
2619
|
-
expect(true).toBe(false);
|
|
2620
|
-
} catch (err: any) {
|
|
2621
|
-
expect(err.message).toMatch(/between 1 and 3/);
|
|
2622
|
-
}
|
|
2623
|
-
});
|
|
2624
|
-
|
|
2625
|
-
test('--scale missing value throws', async () => {
|
|
2626
|
-
try {
|
|
2627
|
-
await handleWriteCommand('viewport', ['--scale'], bm);
|
|
2628
|
-
expect(true).toBe(false);
|
|
2629
|
-
} catch (err: any) {
|
|
2630
|
-
expect(err.message).toMatch(/missing value/);
|
|
2631
|
-
}
|
|
2632
|
-
});
|
|
2633
|
-
|
|
2634
|
-
test('viewport with neither arg nor flag throws usage', async () => {
|
|
2635
|
-
try {
|
|
2636
|
-
await handleWriteCommand('viewport', [], bm);
|
|
2637
|
-
expect(true).toBe(false);
|
|
2638
|
-
} catch (err: any) {
|
|
2639
|
-
expect(err.message).toMatch(/Usage: browse viewport/);
|
|
2640
|
-
}
|
|
2641
|
-
});
|
|
2642
|
-
});
|
|
2643
|
-
|
|
2644
|
-
describe('viewport auto (unpin a pinned size)', () => {
|
|
2645
|
-
test('headless: viewport 375x812 then auto returns to the 1280x720 default', async () => {
|
|
2646
|
-
await handleWriteCommand('goto', [baseUrl + '/basic.html'], bm);
|
|
2647
|
-
await handleWriteCommand('viewport', ['375x812'], bm);
|
|
2648
|
-
expect(await handleWriteCommand('viewport', ['auto'], bm)).toBe('Viewport reset to default 1280x720');
|
|
2649
|
-
expect(bm.getPage().viewportSize()).toEqual({ width: 1280, height: 720 });
|
|
2650
|
-
expect(await bm.getPage().evaluate(() => window.innerWidth)).toBe(1280);
|
|
2651
|
-
expect(bm.getCurrentViewport()).toEqual({ width: 1280, height: 720 });
|
|
2652
|
-
});
|
|
2653
|
-
|
|
2654
|
-
test('reset and unpin are aliases', async () => {
|
|
2655
|
-
for (const alias of ['reset', 'unpin']) {
|
|
2656
|
-
await handleWriteCommand('viewport', ['400x300'], bm);
|
|
2657
|
-
await handleWriteCommand('viewport', [alias], bm);
|
|
2658
|
-
expect(bm.getPage().viewportSize()).toEqual({ width: 1280, height: 720 });
|
|
2659
|
-
}
|
|
2660
|
-
});
|
|
2661
|
-
|
|
2662
|
-
test('auto rejects --scale', async () => {
|
|
2663
|
-
await expect(handleWriteCommand('viewport', ['auto', '--scale', '2'], bm)).rejects.toThrow(/cannot be combined/);
|
|
2664
|
-
});
|
|
2665
|
-
|
|
2666
|
-
test('handler reports which reset the manager performed', async () => {
|
|
2667
|
-
for (const [mode, expected] of [['window', /follows the browser window/], ['default', /default 1280x720/]] as const) {
|
|
2668
|
-
const fakeBm = { resetViewport: async () => mode } as unknown as BrowserManager;
|
|
2669
|
-
expect(await _handleWriteCommand('viewport', ['auto'], bm.getActiveSession(), fakeBm)).toMatch(expected);
|
|
2670
|
-
}
|
|
2671
|
-
});
|
|
2672
|
-
|
|
2673
|
-
test('headed: auto replaces the pinned page with a window-following one in the same tab', async () => {
|
|
2674
|
-
const context = await (bm as any).browser.newContext({ viewport: null });
|
|
2675
|
-
const headed = new BrowserManager();
|
|
2676
|
-
Object.assign(headed as any, { connectionMode: 'headed', context });
|
|
2677
|
-
try {
|
|
2678
|
-
const tabId = await headed.newTab(baseUrl + '/basic.html');
|
|
2679
|
-
await handleWriteCommand('viewport', ['600x400'], headed);
|
|
2680
|
-
const pinned = headed.getPage();
|
|
2681
|
-
expect(pinned.viewportSize()).toEqual({ width: 600, height: 400 });
|
|
2682
|
-
|
|
2683
|
-
expect(await handleWriteCommand('viewport', ['auto'], headed)).toMatch(/follows the browser window/);
|
|
2684
|
-
const fresh = headed.getPage();
|
|
2685
|
-
expect(fresh).not.toBe(pinned);
|
|
2686
|
-
expect(pinned.isClosed()).toBe(true);
|
|
2687
|
-
expect(fresh.viewportSize()).toBeNull();
|
|
2688
|
-
expect(fresh.url()).toBe(baseUrl + '/basic.html');
|
|
2689
|
-
expect(headed.getActiveTabId()).toBe(tabId);
|
|
2690
|
-
expect(headed.getTabCount()).toBe(1);
|
|
2691
|
-
|
|
2692
|
-
await headed.getActiveSession().setTabContent('<p id="kept">kept</p>');
|
|
2693
|
-
await handleWriteCommand('viewport', ['500x300'], headed);
|
|
2694
|
-
await handleWriteCommand('viewport', ['auto'], headed);
|
|
2695
|
-
expect(await headed.getPage().textContent('#kept')).toBe('kept');
|
|
2696
|
-
expect(headed.getPage().viewportSize()).toBeNull();
|
|
2697
|
-
} finally {
|
|
2698
|
-
await context.close();
|
|
2699
|
-
}
|
|
2700
|
-
});
|
|
2701
|
-
});
|
|
2702
|
-
|
|
2703
|
-
// ─── setContent replay across context recreation ────────────────
|
|
2704
|
-
|
|
2705
|
-
describe('setContent replay (load-html survives viewport --scale)', () => {
|
|
2706
|
-
const tmpDir = '/tmp';
|
|
2707
|
-
|
|
2708
|
-
test('load-html → viewport --scale 2 → content survives', async () => {
|
|
2709
|
-
const fix = path.join(tmpDir, `replay-${Date.now()}.html`);
|
|
2710
|
-
fs.writeFileSync(fix, '<h1 id="marker">replay-test-marker</h1>');
|
|
2711
|
-
try {
|
|
2712
|
-
await handleWriteCommand('load-html', [fix], bm);
|
|
2713
|
-
await handleWriteCommand('viewport', ['400x300', '--scale', '2'], bm);
|
|
2714
|
-
const text = await handleReadCommand('text', [], bm);
|
|
2715
|
-
expect(text).toContain('replay-test-marker');
|
|
2716
|
-
await handleWriteCommand('viewport', ['1280x720', '--scale', '1'], bm);
|
|
2717
|
-
} finally {
|
|
2718
|
-
try { fs.unlinkSync(fix); } catch {}
|
|
2719
|
-
}
|
|
2720
|
-
});
|
|
2721
|
-
|
|
2722
|
-
test('double scale cycle: 2x → 1.5x, content still survives', async () => {
|
|
2723
|
-
const fix = path.join(tmpDir, `replay2-${Date.now()}.html`);
|
|
2724
|
-
fs.writeFileSync(fix, '<h2 id="m">double-cycle-marker</h2>');
|
|
2725
|
-
try {
|
|
2726
|
-
await handleWriteCommand('load-html', [fix], bm);
|
|
2727
|
-
await handleWriteCommand('viewport', ['400x300', '--scale', '2'], bm);
|
|
2728
|
-
await handleWriteCommand('viewport', ['400x300', '--scale', '1.5'], bm);
|
|
2729
|
-
const text = await handleReadCommand('text', [], bm);
|
|
2730
|
-
expect(text).toContain('double-cycle-marker');
|
|
2731
|
-
await handleWriteCommand('viewport', ['1280x720', '--scale', '1'], bm);
|
|
2732
|
-
} finally {
|
|
2733
|
-
try { fs.unlinkSync(fix); } catch {}
|
|
2734
|
-
}
|
|
2735
|
-
});
|
|
2736
|
-
|
|
2737
|
-
test('goto clears loadedHtml — subsequent viewport --scale does NOT resurrect old HTML', async () => {
|
|
2738
|
-
const fix = path.join(tmpDir, `clear-${Date.now()}.html`);
|
|
2739
|
-
fs.writeFileSync(fix, '<div id="stale">stale-content</div>');
|
|
2740
|
-
try {
|
|
2741
|
-
await handleWriteCommand('load-html', [fix], bm);
|
|
2742
|
-
await handleWriteCommand('goto', [baseUrl + '/basic.html'], bm);
|
|
2743
|
-
await handleWriteCommand('viewport', ['400x300', '--scale', '2'], bm);
|
|
2744
|
-
const text = await handleReadCommand('text', [], bm);
|
|
2745
|
-
// Should see basic.html content, NOT the stale load-html content
|
|
2746
|
-
expect(text).not.toContain('stale-content');
|
|
2747
|
-
await handleWriteCommand('viewport', ['1280x720', '--scale', '1'], bm);
|
|
2748
|
-
} finally {
|
|
2749
|
-
try { fs.unlinkSync(fix); } catch {}
|
|
2750
|
-
}
|
|
2751
|
-
});
|
|
2752
|
-
});
|
|
2753
|
-
|
|
2754
|
-
// ─── Alias routing ─────────────────────────────────────────────
|
|
2755
|
-
|
|
2756
|
-
describe('Command aliases', () => {
|
|
2757
|
-
const tmpDir = '/tmp';
|
|
2758
|
-
const aliasFix = path.join(tmpDir, `alias-${Date.now()}.html`);
|
|
2759
|
-
|
|
2760
|
-
beforeAll(() => {
|
|
2761
|
-
fs.writeFileSync(aliasFix, '<p id="alias">alias routing ok</p>');
|
|
2762
|
-
});
|
|
2763
|
-
afterAll(() => {
|
|
2764
|
-
try { fs.unlinkSync(aliasFix); } catch {}
|
|
2765
|
-
});
|
|
2766
|
-
|
|
2767
|
-
test('setcontent alias routes to load-html via chain', async () => {
|
|
2768
|
-
// Chain canonicalizes aliases end-to-end; verifies the dispatch path
|
|
2769
|
-
const result = await chainMeta(bm, [JSON.stringify([['setcontent', aliasFix]])]);
|
|
2770
|
-
expect(result).toContain('Loaded HTML:');
|
|
2771
|
-
const text = await handleReadCommand('text', [], bm);
|
|
2772
|
-
expect(text).toContain('alias routing ok');
|
|
2773
|
-
});
|
|
2774
|
-
|
|
2775
|
-
test('set-content (hyphenated) alias also routes', async () => {
|
|
2776
|
-
const result = await chainMeta(bm, [JSON.stringify([['set-content', aliasFix]])]);
|
|
2777
|
-
expect(result).toContain('Loaded HTML:');
|
|
2778
|
-
});
|
|
2779
|
-
});
|