izanagi-ai 2.3.1 → 2.3.2
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/package.json +1 -1
- package/skills/tdd/SKILL.md +5 -1
- package/skills/tdd/references/writing-good-tests.md +198 -0
- package/skills/tdd/references.md +1 -1
- package/skills/webapp-testing/SKILL.md +12 -1
- package/skills/webapp-testing/examples/console_logging.py +36 -0
- package/skills/webapp-testing/examples/element_discovery.py +42 -0
- package/skills/webapp-testing/examples/static_html_automation.py +34 -0
- package/skills/webapp-testing/examples/with_server.py +106 -0
- package/skills/webapp-testing/references.md +1 -1
package/package.json
CHANGED
package/skills/tdd/SKILL.md
CHANGED
|
@@ -72,7 +72,11 @@ Bug encontrado? Escreva teste falhando que reproduz o bug → siga o ciclo → o
|
|
|
72
72
|
|
|
73
73
|
Não marcou todos? Você pulou TDD. Recomece.
|
|
74
74
|
|
|
75
|
+
## Testes bons (referência local)
|
|
76
|
+
|
|
77
|
+
Antes de escrever ou mudar testes, leia `references/writing-good-tests.md` — regras de testes honestos: nomeie a quebra que o teste pega (bug, não decisão), derive expectativas à mão (nunca com o código sob teste), mock só o nível lento/externo, mocks espelham a estrutura real, e rode o **mutation check** antes de terminar.
|
|
78
|
+
|
|
75
79
|
## References
|
|
76
80
|
|
|
77
|
-
- Repo original: [obra/superpowers](https://github.com/obra/superpowers) — skill `skills/test-driven-development/SKILL.md` (+ `writing-good-tests.md`).
|
|
81
|
+
- Repo original: [obra/superpowers](https://github.com/obra/superpowers) — skill `skills/test-driven-development/SKILL.md` (+ `writing-good-tests.md`, portado localmente em `references/writing-good-tests.md`).
|
|
78
82
|
- Curadoria completa em `references.md`.
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
# Writing Good Tests
|
|
2
|
+
|
|
3
|
+
**Load this reference when:** writing or changing tests, adding mocks, or
|
|
4
|
+
adding cleanup/helper methods for tests.
|
|
5
|
+
|
|
6
|
+
## Overview
|
|
7
|
+
|
|
8
|
+
A test exists to catch a specific break. Two principles govern everything
|
|
9
|
+
here:
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
1. Every test names the break it catches
|
|
13
|
+
2. Every test exercises the real thing
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Strict TDD produces both naturally: a test written first and watched
|
|
17
|
+
failing against real code has already proven it can fail, and only earns
|
|
18
|
+
a mock when the real dependency proves slow or external.
|
|
19
|
+
|
|
20
|
+
## Principle 1: Name the Break
|
|
21
|
+
|
|
22
|
+
Before writing the test body, answer: **what production change should
|
|
23
|
+
make this test fail — and is that change a bug or a decision?** A test
|
|
24
|
+
earns its place by catching a wrong branch, missing side effect, wrong
|
|
25
|
+
argument, boundary case, or broken contract.
|
|
26
|
+
|
|
27
|
+
**Derive expectations independently.** Use literals and hand-checked
|
|
28
|
+
fixtures; table-driven tests with literal `want` values are the preferred
|
|
29
|
+
shape. An expectation computed by the code under test — or its helpers —
|
|
30
|
+
passes no matter what that code does:
|
|
31
|
+
|
|
32
|
+
```typescript
|
|
33
|
+
// ❌ Mirror assertion: the same builder computes both sides — always true
|
|
34
|
+
const expected = buildSearchQuery({ tag: 'urgent' });
|
|
35
|
+
expect(buildSearchQuery({ tag: 'urgent' })).toBe(expected);
|
|
36
|
+
|
|
37
|
+
// ✅ Hand-derived literal
|
|
38
|
+
expect(buildSearchQuery({ tag: 'urgent' })).toBe('tag:"urgent"');
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
**No change detectors.** If only intentional decisions can fail a test —
|
|
42
|
+
a constant's value, exact message wording, private structure — it fires
|
|
43
|
+
on redesign and sleeps through bugs. Test the behavior that depends on
|
|
44
|
+
the decision: not `expect(MAX_RETRIES).toBe(5)` but "a failing call is
|
|
45
|
+
retried 5 times and the 6th attempt never happens."
|
|
46
|
+
|
|
47
|
+
**Behavior, not text.** Asserting that a script, skill, or config
|
|
48
|
+
contains an exact line proves only that the source is the source. Run
|
|
49
|
+
scripts against controlled inputs and assert outputs, side effects, or
|
|
50
|
+
exit codes. Documents that instruct agents are tested by the consuming
|
|
51
|
+
agent's behavior (superpowers:writing-skills); prose for humans earns no
|
|
52
|
+
test at all.
|
|
53
|
+
|
|
54
|
+
**Your code, not the framework.** Test the contract your code makes at
|
|
55
|
+
its boundaries — the route you register, the query you emit, the payload
|
|
56
|
+
you produce. Upstream mechanics are their maintainers' tests to write
|
|
57
|
+
(the classic: asserting your router invokes a registered handler — that
|
|
58
|
+
is the framework's test, not yours). When upstream behavior genuinely
|
|
59
|
+
surprised you, write one narrow characterization test naming the
|
|
60
|
+
assumption. The same boundary applies inside your code: constructors,
|
|
61
|
+
getters, constants, and trivial forwarding earn tests only when they
|
|
62
|
+
validate, normalize, default, derive, enforce, or cause side effects —
|
|
63
|
+
otherwise assert the first consumer-visible result that depends on them.
|
|
64
|
+
|
|
65
|
+
### Gate Function
|
|
66
|
+
|
|
67
|
+
```
|
|
68
|
+
BEFORE writing the test body:
|
|
69
|
+
Name the production change that would make this test fail.
|
|
70
|
+
|
|
71
|
+
Cannot name one → redesign around an observable behavior
|
|
72
|
+
"The source text changed" → run the artifact and assert its effects
|
|
73
|
+
Only intentional decisions → change detector; test the behavior
|
|
74
|
+
that depends on the decision
|
|
75
|
+
|
|
76
|
+
Confirm the expected value is derived without the code under test.
|
|
77
|
+
IF it reuses the code's logic or helpers:
|
|
78
|
+
Replace it with a literal or hand-checked fixture
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Principle 2: Exercise the Real Thing
|
|
82
|
+
|
|
83
|
+
**The mock earns no assertions.** A mock assertion passes when the mock
|
|
84
|
+
is present and fails when it is absent — it says nothing about the
|
|
85
|
+
component. Assert the real component's behavior; if the mock is what you
|
|
86
|
+
are checking, unmock it or delete the assertion.
|
|
87
|
+
|
|
88
|
+
```typescript
|
|
89
|
+
// ✅ Real behavior
|
|
90
|
+
expect(screen.getByRole('navigation')).toBeInTheDocument();
|
|
91
|
+
|
|
92
|
+
// ❌ Mock existence
|
|
93
|
+
expect(screen.getByTestId('sidebar-mock')).toBeInTheDocument();
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
**your human partner's correction:** "Are we testing the behavior of a
|
|
97
|
+
mock?"
|
|
98
|
+
|
|
99
|
+
**Mock at the right level.** Learn every side effect of the real method
|
|
100
|
+
before replacing it; mock the slow or external operation and keep what
|
|
101
|
+
the test depends on real. When unsure, run the test against the real
|
|
102
|
+
implementation first and observe what actually needs to happen.
|
|
103
|
+
|
|
104
|
+
```typescript
|
|
105
|
+
// ❌ The mock swallows the config write that duplicate detection reads
|
|
106
|
+
vi.mock('ToolCatalog', () => ({
|
|
107
|
+
discoverAndCacheTools: vi.fn().mockResolvedValue(undefined)
|
|
108
|
+
}));
|
|
109
|
+
|
|
110
|
+
// ✅ Mock only the slow server startup; the config write stays real
|
|
111
|
+
vi.mock('MCPServerManager');
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
**Make doubles specific.** When arguments, call counts, or ordering are
|
|
115
|
+
part of the contract, assert them — a fake that accepts anything verifies
|
|
116
|
+
nothing. Give each branch (success, error, malformed) its own fixture or
|
|
117
|
+
spy, so the wrong branch cannot satisfy the expectation.
|
|
118
|
+
|
|
119
|
+
**Mirror real data completely.** Mock the complete structure as it exists
|
|
120
|
+
in reality — all documented fields — not just the ones your test reads.
|
|
121
|
+
Partial mocks fail silently when downstream code reads an omitted field:
|
|
122
|
+
the test passes while integration breaks.
|
|
123
|
+
|
|
124
|
+
**Production classes carry production methods only.** Cleanup that only
|
|
125
|
+
tests need lives in test utilities, never as a `destroy()` on the
|
|
126
|
+
production class. Ask: is this method called only from tests? Does this
|
|
127
|
+
class own this resource's lifecycle? Wrong answers → test utility.
|
|
128
|
+
|
|
129
|
+
**Prefer real components over complex mocks.** When mock setup outgrows
|
|
130
|
+
the test logic, mocks miss methods the real components have, or tests
|
|
131
|
+
break when the mock changes, switch to an integration test with real
|
|
132
|
+
components. **your human partner's question:** "Do we need to be using a
|
|
133
|
+
mock here?"
|
|
134
|
+
|
|
135
|
+
### Gate Function
|
|
136
|
+
|
|
137
|
+
```
|
|
138
|
+
BEFORE adding a mock or test helper:
|
|
139
|
+
List the real method's side effects; keep the ones the test
|
|
140
|
+
depends on real — mock the slow/external level below them.
|
|
141
|
+
|
|
142
|
+
Mock responses mirror the complete real structure.
|
|
143
|
+
|
|
144
|
+
A method only tests call lives in test utilities, not production.
|
|
145
|
+
|
|
146
|
+
About to assert on the mock itself?
|
|
147
|
+
Unmock it or delete the assertion.
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
## Tests Ship With the Implementation
|
|
151
|
+
|
|
152
|
+
The TDD cycle — failing test, minimal implementation, refactor — is what
|
|
153
|
+
"complete" means. Ship the tests the behavior needs and only those:
|
|
154
|
+
trivial code and human prose earn none, and a test written to satisfy
|
|
155
|
+
process costs maintenance forever.
|
|
156
|
+
|
|
157
|
+
## The Mutation Check
|
|
158
|
+
|
|
159
|
+
Before finishing, mentally mutate the production code; at least one test
|
|
160
|
+
should fail for each realistic mutation:
|
|
161
|
+
|
|
162
|
+
- Wrong constant or argument
|
|
163
|
+
- Wrong branch handler
|
|
164
|
+
- Missing state change or side effect
|
|
165
|
+
- Empty or default return
|
|
166
|
+
- Missing validation for zero, empty, nil, unauthorized, or malformed input
|
|
167
|
+
|
|
168
|
+
A mutation nothing catches marks the behavior as unprotected — or the
|
|
169
|
+
test as tautological.
|
|
170
|
+
|
|
171
|
+
## Quick Reference
|
|
172
|
+
|
|
173
|
+
| When you... | Do |
|
|
174
|
+
|-------------|-----|
|
|
175
|
+
| Write any test | Name the break it catches — a bug, not a decision |
|
|
176
|
+
| Build an expected value | Derive it by hand; never with the code under test |
|
|
177
|
+
| Test a script or document | Run it / pressure-test its consumer; never grep its text |
|
|
178
|
+
| Reach for a dependency test | Test your boundary contract, not their documented mechanics |
|
|
179
|
+
| Want to assert on a mocked element | Test the real component, or unmock it |
|
|
180
|
+
| Are about to mock a method | Learn its side effects; mock the slow/external level |
|
|
181
|
+
| Build a mock response | Mirror the real structure completely |
|
|
182
|
+
| Need cleanup only tests use | Put it in test utilities |
|
|
183
|
+
| Watch mock setup balloon | Switch to an integration test with real components |
|
|
184
|
+
| Finish a test file | Run the mutation check |
|
|
185
|
+
|
|
186
|
+
## Warning Signs
|
|
187
|
+
|
|
188
|
+
- Setup and assertion share the same object, guaranteeing equality
|
|
189
|
+
- The test can fail only through a panic, crash, or missing selector
|
|
190
|
+
- The test fails on every intentional change, never on accidental breakage
|
|
191
|
+
- Expected values are hidden behind loops, builders, or helpers
|
|
192
|
+
- The test greps source text, or asserts a removed symbol stays removed
|
|
193
|
+
- The test would still matter if only the framework remained
|
|
194
|
+
- The test exists for coverage, checking no side effect or outcome
|
|
195
|
+
- An assertion checks a `*-mock` test ID, or fails if you remove the mock
|
|
196
|
+
- A method is called only from test files
|
|
197
|
+
- Mock setup is more than half the test, or you can't explain why the mock is needed
|
|
198
|
+
- Mocking "just to be safe"
|
package/skills/tdd/references.md
CHANGED
|
@@ -6,7 +6,7 @@ Curadoria da skill TDD do framework Superpowers.
|
|
|
6
6
|
|
|
7
7
|
- **Repositório**: https://github.com/obra/superpowers — 264k+ stars, MIT
|
|
8
8
|
- **Skill original**: `skills/test-driven-development/SKILL.md`
|
|
9
|
-
- **Auxiliar**: `skills/test-driven-development/writing-good-tests.md` — regras para testes honestos (nomeie a mudança de produção que faria o teste falhar; asserts em comportamento real; helpers só em código de teste...)
|
|
9
|
+
- **Auxiliar**: `skills/test-driven-development/writing-good-tests.md` — regras para testes honestos (nomeie a mudança de produção que faria o teste falhar; asserts em comportamento real; helpers só em código de teste...) — **portado localmente em `references/writing-good-tests.md`**
|
|
10
10
|
|
|
11
11
|
## Aproveitado no Izanagi
|
|
12
12
|
|
|
@@ -69,8 +69,19 @@ with sync_playwright() as p:
|
|
|
69
69
|
- Waits: `wait_for_selector()` / `wait_for_timeout()` quando necessário.
|
|
70
70
|
- Para fluxos complexos múltiplos servidores: gerencie ambos (backend + frontend).
|
|
71
71
|
|
|
72
|
+
## Exemplos locais (`examples/`)
|
|
73
|
+
|
|
74
|
+
Scripts de referência (Playwright Python; use como caixa-preta — não edite a menos que necessário):
|
|
75
|
+
|
|
76
|
+
- `with_server.py` — sobe 1+ servidores locais, espera as portas ficarem prontas e roda seu script. Uso: `python examples/with_server.py --server "npm run dev" --port 5173 -- python examples/element_discovery.py`
|
|
77
|
+
- `element_discovery.py` — descobre botões/links/inputs no estado renderizado + screenshot full-page.
|
|
78
|
+
- `console_logging.py` — captura mensagens do console do navegador durante a automação.
|
|
79
|
+
- `static_html_automation.py` — automação de arquivos HTML estáticos via `file://`.
|
|
80
|
+
|
|
81
|
+
Requisitos: `pip install playwright` + `playwright install chromium`. Saídas (screenshots/logs) vão para `outputs/` do projeto.
|
|
82
|
+
|
|
72
83
|
## References
|
|
73
84
|
|
|
74
|
-
- Repo original: [ComposioHQ/awesome-claude-skills](https://github.com/ComposioHQ/awesome-claude-skills) — skill `webapp-testing/` (índice curado, 66k stars)
|
|
85
|
+
- Repo original: [ComposioHQ/awesome-claude-skills](https://github.com/ComposioHQ/awesome-claude-skills) — skill `webapp-testing/` (índice curado, 66k stars); scripts portados localmente em `examples/`.
|
|
75
86
|
- Playwright docs: https://playwright.dev/docs/intro
|
|
76
87
|
- Curadoria completa em `references.md`.
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
from playwright.sync_api import sync_playwright
|
|
2
|
+
import os
|
|
3
|
+
|
|
4
|
+
# Example: Capturing console logs during browser automation
|
|
5
|
+
url = 'http://localhost:5173' # Replace with your URL
|
|
6
|
+
|
|
7
|
+
console_logs = []
|
|
8
|
+
os.makedirs('outputs', exist_ok=True)
|
|
9
|
+
|
|
10
|
+
with sync_playwright() as p:
|
|
11
|
+
browser = p.chromium.launch(headless=True)
|
|
12
|
+
page = browser.new_page(viewport={'width': 1920, 'height': 1080})
|
|
13
|
+
|
|
14
|
+
# Set up console log capture
|
|
15
|
+
def handle_console_message(msg):
|
|
16
|
+
console_logs.append(f"[{msg.type}] {msg.text}")
|
|
17
|
+
print(f"Console: [{msg.type}] {msg.text}")
|
|
18
|
+
|
|
19
|
+
page.on("console", handle_console_message)
|
|
20
|
+
|
|
21
|
+
# Navigate to page
|
|
22
|
+
page.goto(url)
|
|
23
|
+
page.wait_for_load_state('networkidle')
|
|
24
|
+
|
|
25
|
+
# Interact with the page (triggers console logs)
|
|
26
|
+
page.click('text=Dashboard')
|
|
27
|
+
page.wait_for_timeout(1000)
|
|
28
|
+
|
|
29
|
+
browser.close()
|
|
30
|
+
|
|
31
|
+
# Save console logs to file
|
|
32
|
+
with open('outputs/console.log', 'w') as f:
|
|
33
|
+
f.write('\n'.join(console_logs))
|
|
34
|
+
|
|
35
|
+
print(f"\nCaptured {len(console_logs)} console messages")
|
|
36
|
+
print("Logs saved to: outputs/console.log")
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
from playwright.sync_api import sync_playwright
|
|
2
|
+
import os
|
|
3
|
+
|
|
4
|
+
# Example: Discovering buttons and other elements on a page
|
|
5
|
+
os.makedirs('outputs', exist_ok=True)
|
|
6
|
+
|
|
7
|
+
with sync_playwright() as p:
|
|
8
|
+
browser = p.chromium.launch(headless=True)
|
|
9
|
+
page = browser.new_page()
|
|
10
|
+
|
|
11
|
+
# Navigate to page and wait for it to fully load
|
|
12
|
+
page.goto('http://localhost:5173')
|
|
13
|
+
page.wait_for_load_state('networkidle')
|
|
14
|
+
|
|
15
|
+
# Discover all buttons on the page
|
|
16
|
+
buttons = page.locator('button').all()
|
|
17
|
+
print(f"Found {len(buttons)} buttons:")
|
|
18
|
+
for i, button in enumerate(buttons):
|
|
19
|
+
text = button.inner_text() if button.is_visible() else "[hidden]"
|
|
20
|
+
print(f" [{i}] {text}")
|
|
21
|
+
|
|
22
|
+
# Discover links
|
|
23
|
+
links = page.locator('a[href]').all()
|
|
24
|
+
print(f"\nFound {len(links)} links:")
|
|
25
|
+
for link in links[:5]: # Show first 5
|
|
26
|
+
text = link.inner_text().strip()
|
|
27
|
+
href = link.get_attribute('href')
|
|
28
|
+
print(f" - {text} -> {href}")
|
|
29
|
+
|
|
30
|
+
# Discover input fields
|
|
31
|
+
inputs = page.locator('input, textarea, select').all()
|
|
32
|
+
print(f"\nFound {len(inputs)} input fields:")
|
|
33
|
+
for input_elem in inputs:
|
|
34
|
+
name = input_elem.get_attribute('name') or input_elem.get_attribute('id') or "[unnamed]"
|
|
35
|
+
input_type = input_elem.get_attribute('type') or 'text'
|
|
36
|
+
print(f" - {name} ({input_type})")
|
|
37
|
+
|
|
38
|
+
# Take screenshot for visual reference
|
|
39
|
+
page.screenshot(path='outputs/page_discovery.png', full_page=True)
|
|
40
|
+
print("\nScreenshot saved to outputs/page_discovery.png")
|
|
41
|
+
|
|
42
|
+
browser.close()
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
from playwright.sync_api import sync_playwright
|
|
2
|
+
import os
|
|
3
|
+
|
|
4
|
+
# Example: Automating interaction with static HTML files using file:// URLs
|
|
5
|
+
html_file_path = os.path.abspath('path/to/your/file.html')
|
|
6
|
+
file_url = f'file://{html_file_path}'
|
|
7
|
+
|
|
8
|
+
os.makedirs('outputs', exist_ok=True)
|
|
9
|
+
|
|
10
|
+
with sync_playwright() as p:
|
|
11
|
+
browser = p.chromium.launch(headless=True)
|
|
12
|
+
page = browser.new_page(viewport={'width': 1920, 'height': 1080})
|
|
13
|
+
|
|
14
|
+
# Navigate to local HTML file
|
|
15
|
+
page.goto(file_url)
|
|
16
|
+
|
|
17
|
+
# Take screenshot
|
|
18
|
+
page.screenshot(path='outputs/static_page.png', full_page=True)
|
|
19
|
+
|
|
20
|
+
# Interact with elements
|
|
21
|
+
page.click('text=Click Me')
|
|
22
|
+
page.fill('#name', 'John Doe')
|
|
23
|
+
page.fill('#email', 'john@example.com')
|
|
24
|
+
|
|
25
|
+
# Submit form
|
|
26
|
+
page.click('button[type="submit"]')
|
|
27
|
+
page.wait_for_timeout(500)
|
|
28
|
+
|
|
29
|
+
# Take final screenshot
|
|
30
|
+
page.screenshot(path='outputs/after_submit.png', full_page=True)
|
|
31
|
+
|
|
32
|
+
browser.close()
|
|
33
|
+
|
|
34
|
+
print("Static HTML automation completed! Outputs saved to outputs/")
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Start one or more servers, wait for them to be ready, run a command, then clean up.
|
|
4
|
+
|
|
5
|
+
Usage:
|
|
6
|
+
# Single server
|
|
7
|
+
python scripts/with_server.py --server "npm run dev" --port 5173 -- python automation.py
|
|
8
|
+
python scripts/with_server.py --server "npm start" --port 3000 -- python test.py
|
|
9
|
+
|
|
10
|
+
# Multiple servers
|
|
11
|
+
python scripts/with_server.py \
|
|
12
|
+
--server "cd backend && python server.py" --port 3000 \
|
|
13
|
+
--server "cd frontend && npm run dev" --port 5173 \
|
|
14
|
+
-- python test.py
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
import subprocess
|
|
18
|
+
import socket
|
|
19
|
+
import time
|
|
20
|
+
import sys
|
|
21
|
+
import argparse
|
|
22
|
+
|
|
23
|
+
def is_server_ready(port, timeout=30):
|
|
24
|
+
"""Wait for server to be ready by polling the port."""
|
|
25
|
+
start_time = time.time()
|
|
26
|
+
while time.time() - start_time < timeout:
|
|
27
|
+
try:
|
|
28
|
+
with socket.create_connection(('localhost', port), timeout=1):
|
|
29
|
+
return True
|
|
30
|
+
except (socket.error, ConnectionRefusedError):
|
|
31
|
+
time.sleep(0.5)
|
|
32
|
+
return False
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def main():
|
|
36
|
+
parser = argparse.ArgumentParser(description='Run command with one or more servers')
|
|
37
|
+
parser.add_argument('--server', action='append', dest='servers', required=True, help='Server command (can be repeated)')
|
|
38
|
+
parser.add_argument('--port', action='append', dest='ports', type=int, required=True, help='Port for each server (must match --server count)')
|
|
39
|
+
parser.add_argument('--timeout', type=int, default=30, help='Timeout in seconds per server (default: 30)')
|
|
40
|
+
parser.add_argument('command', nargs=argparse.REMAINDER, help='Command to run after server(s) ready')
|
|
41
|
+
|
|
42
|
+
args = parser.parse_args()
|
|
43
|
+
|
|
44
|
+
# Remove the '--' separator if present
|
|
45
|
+
if args.command and args.command[0] == '--':
|
|
46
|
+
args.command = args.command[1:]
|
|
47
|
+
|
|
48
|
+
if not args.command:
|
|
49
|
+
print("Error: No command specified to run")
|
|
50
|
+
sys.exit(1)
|
|
51
|
+
|
|
52
|
+
# Parse server configurations
|
|
53
|
+
if len(args.servers) != len(args.ports):
|
|
54
|
+
print("Error: Number of --server and --port arguments must match")
|
|
55
|
+
sys.exit(1)
|
|
56
|
+
|
|
57
|
+
servers = []
|
|
58
|
+
for cmd, port in zip(args.servers, args.ports):
|
|
59
|
+
servers.append({'cmd': cmd, 'port': port})
|
|
60
|
+
|
|
61
|
+
server_processes = []
|
|
62
|
+
|
|
63
|
+
try:
|
|
64
|
+
# Start all servers
|
|
65
|
+
for i, server in enumerate(servers):
|
|
66
|
+
print(f"Starting server {i+1}/{len(servers)}: {server['cmd']}")
|
|
67
|
+
|
|
68
|
+
# Use shell=True to support commands with cd and &&
|
|
69
|
+
process = subprocess.Popen(
|
|
70
|
+
server['cmd'],
|
|
71
|
+
shell=True,
|
|
72
|
+
stdout=subprocess.PIPE,
|
|
73
|
+
stderr=subprocess.PIPE
|
|
74
|
+
)
|
|
75
|
+
server_processes.append(process)
|
|
76
|
+
|
|
77
|
+
# Wait for this server to be ready
|
|
78
|
+
print(f"Waiting for server on port {server['port']}...")
|
|
79
|
+
if not is_server_ready(server['port'], timeout=args.timeout):
|
|
80
|
+
raise RuntimeError(f"Server failed to start on port {server['port']} within {args.timeout}s")
|
|
81
|
+
|
|
82
|
+
print(f"Server ready on port {server['port']}")
|
|
83
|
+
|
|
84
|
+
print(f"\nAll {len(servers)} server(s) ready")
|
|
85
|
+
|
|
86
|
+
# Run the command
|
|
87
|
+
print(f"Running: {' '.join(args.command)}\n")
|
|
88
|
+
result = subprocess.run(args.command)
|
|
89
|
+
sys.exit(result.returncode)
|
|
90
|
+
|
|
91
|
+
finally:
|
|
92
|
+
# Clean up all servers
|
|
93
|
+
print(f"\nStopping {len(server_processes)} server(s)...")
|
|
94
|
+
for i, process in enumerate(server_processes):
|
|
95
|
+
try:
|
|
96
|
+
process.terminate()
|
|
97
|
+
process.wait(timeout=5)
|
|
98
|
+
except subprocess.TimeoutExpired:
|
|
99
|
+
process.kill()
|
|
100
|
+
process.wait()
|
|
101
|
+
print(f"Server {i+1} stopped")
|
|
102
|
+
print("All servers stopped")
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
if __name__ == '__main__':
|
|
106
|
+
main()
|
|
@@ -5,7 +5,7 @@ Curadoria de automação de testes web com navegador.
|
|
|
5
5
|
## Fonte principal
|
|
6
6
|
|
|
7
7
|
- **Índice**: https://github.com/ComposioHQ/awesome-claude-skills — 66k stars, maior lista curada de Claude Skills
|
|
8
|
-
- **Skill original**: `webapp-testing/` no repo (scripts Python `with_server.py`, `element_discovery.py`, exemplos)
|
|
8
|
+
- **Skill original**: `webapp-testing/` no repo (scripts Python `with_server.py`, `element_discovery.py`, exemplos) — **portados localmente em `examples/`** (paths de saída adaptados para `outputs/` do projeto)
|
|
9
9
|
- **Playwright**: https://playwright.dev/docs/intro — docs oficiais (Node, Python, .NET, Java)
|
|
10
10
|
|
|
11
11
|
## O que aproveitar no Izanagi
|