pi-ui-extend 1.0.19 → 1.0.21
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/external/pi-tools-suite/README.md +17 -9
- package/external/pi-tools-suite/docs/browser-qa-subagent.md +39 -8
- package/external/pi-tools-suite/package.json +1 -0
- package/external/pi-tools-suite/src/async-subagents/core/config.ts +7 -1
- package/external/pi-tools-suite/src/async-subagents/core/spawn.ts +21 -3
- package/external/pi-tools-suite/src/async-subagents/private-skills/browser-qa/SKILL.md +64 -15
- package/external/pi-tools-suite/src/async-subagents/private-skills/browser-qa/references/qa-design.md +106 -0
- package/external/pi-tools-suite/src/async-subagents/private-skills/browser-qa/scripts/browser-qa-runner.mjs +55 -8
- package/external/pi-tools-suite/src/todo/state/replay.ts +28 -10
- package/external/pi-tools-suite/src/todo/todo.ts +28 -6
- package/package.json +3 -1
|
@@ -251,8 +251,11 @@ The built-in `browser-qa` role runs on `antigravity/gemini-3-flash-preview`, wit
|
|
|
251
251
|
`openai-codex/gpt-5.4-mini` as its fallback. Its browser workflow is an explicit
|
|
252
252
|
private skill under `src/async-subagents/private-skills/`, outside normal Pi skill
|
|
253
253
|
discovery. The role's first-class `isolatedSkills` setting launches the child with
|
|
254
|
-
`--no-skills` plus
|
|
255
|
-
|
|
254
|
+
`--no-skills` plus one self-contained private workflow. It bundles the relevant
|
|
255
|
+
scenario-design, locator, waiting, assertion, evidence, and cleanup guidance next
|
|
256
|
+
to its trusted runner, so browser QA does not depend on a separately installed
|
|
257
|
+
skill or CLI. The private workflow remains mandatory when configuration appends
|
|
258
|
+
other isolated skills; parent and ordinary sub-agent sessions do not discover it.
|
|
256
259
|
|
|
257
260
|
Keep named dev/staging auth profiles in project `.pi/qa_auth.jsonc` (there is no
|
|
258
261
|
`/qa-auth` command). The private runner supports `form`, `cookie`, `localStorage`,
|
|
@@ -287,13 +290,18 @@ blocked. Example:
|
|
|
287
290
|
|
|
288
291
|
Do not place credential values in prompts, QA flows, shell arguments, reports,
|
|
289
292
|
or evidence. The helper reads JSONC internally, emits only redacted statuses, and
|
|
290
|
-
caches generated storage state under `.pi/qa-auth-state`.
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
293
|
+
caches generated storage state under `.pi/qa-auth-state`. The launcher provides
|
|
294
|
+
each browser QA process with its own
|
|
295
|
+
`.pi/subagents/<run>/<agent-id>/browser-qa/` workspace. Declarative flows,
|
|
296
|
+
screenshots, video, sanitized traces, and result manifests stay there, so normal
|
|
297
|
+
session shutdown or `subagents cleanup` removes them with the run directory.
|
|
298
|
+
The runner validates the owning agent metadata and refuses flows outside that
|
|
299
|
+
workspace; reusing an agent id clears stale browser QA files first. Trace archives
|
|
300
|
+
have network records and non-image resources removed, then known
|
|
301
|
+
configured/runtime credential values are redacted and verified before retention.
|
|
302
|
+
Missing, ambiguous, rejected, or expired auth returns `QA_PROFILE_REQUIRED` or
|
|
303
|
+
`QA_AUTH_UPDATE_REQUIRED`, naming only the profile/file/reason needed for the
|
|
304
|
+
parent to ask the user for an update and rerun. See
|
|
297
305
|
`src/async-subagents/private-skills/browser-qa/references/qa-auth.example.jsonc`
|
|
298
306
|
for complete profile shapes and `references/qa-flow.example.jsonc` beside it for
|
|
299
307
|
the declarative, non-executable QA action/assertion format.
|
|
@@ -17,6 +17,10 @@ evidence. The role uses `antigravity/gemini-3-flash-preview`, falling back to
|
|
|
17
17
|
Antigravity-backed role unavailable.
|
|
18
18
|
- A type profile may declare `isolatedSkills`. Spawning that profile adds
|
|
19
19
|
`--no-skills` followed by one explicit `--skill` per configured path.
|
|
20
|
+
- The `browser-qa` profile always loads one self-contained private workflow.
|
|
21
|
+
Relevant browser-test design guidance is bundled beside its trusted runner;
|
|
22
|
+
no separately discovered skill or browser CLI is required. Configuration may
|
|
23
|
+
append isolated skills but cannot remove the mandatory private workflow.
|
|
20
24
|
- Other sub-agent profiles and the parent session must not discover the private
|
|
21
25
|
skill automatically.
|
|
22
26
|
|
|
@@ -37,9 +41,11 @@ evidence. The role uses `antigravity/gemini-3-flash-preview`, falling back to
|
|
|
37
41
|
- The bundled runner reads secrets internally. Credentials must never be copied
|
|
38
42
|
into prompts, generated QA flows, shell arguments, transcripts, reports,
|
|
39
43
|
or QA evidence.
|
|
40
|
-
- Generated browser state is private cache under `.pi/qa-auth-state
|
|
41
|
-
|
|
42
|
-
|
|
44
|
+
- Generated browser state is private cache under `.pi/qa-auth-state`. Ephemeral
|
|
45
|
+
flows, evidence, and result manifests are written under the owning agent's
|
|
46
|
+
`.pi/subagents/<run>/<agent-id>/browser-qa/` workspace. Multiple profiles use
|
|
47
|
+
separate browser contexts/evidence directories, and normal sub-agent shutdown
|
|
48
|
+
or cleanup removes the whole workspace with its run.
|
|
43
49
|
- Missing, ambiguous, rejected, or expired auth returns a machine-readable
|
|
44
50
|
update-required/profile-required status naming only the profile id, config
|
|
45
51
|
file, and redacted reason. The parent asks the user to update the file and
|
|
@@ -51,6 +57,10 @@ evidence. The role uses `antigravity/gemini-3-flash-preview`, falling back to
|
|
|
51
57
|
trusted runner implements a bounded set of navigation, interaction,
|
|
52
58
|
assertion, screenshot, and auth-rejection actions and never gives the flow a
|
|
53
59
|
Playwright context or credential values.
|
|
60
|
+
- The launcher injects `PI_SUBAGENT_AGENT_DIR`, pre-creates a private
|
|
61
|
+
`browser-qa/flows/` workspace, and clears stale browser QA files when an agent
|
|
62
|
+
id is reused. The runner validates the directory's project/type metadata and
|
|
63
|
+
refuses flows outside it; the model cannot select a shared evidence root.
|
|
54
64
|
- The runner owns browser lifecycle, origin checks, auth application, tracing,
|
|
55
65
|
screenshots, video finalization, and redacted result output. Before retaining
|
|
56
66
|
a trace it removes network/non-image resource entries, redacts configured and
|
|
@@ -66,9 +76,10 @@ evidence. The role uses `antigravity/gemini-3-flash-preview`, falling back to
|
|
|
66
76
|
|
|
67
77
|
## Acceptance criteria
|
|
68
78
|
|
|
69
|
-
1. `browser-qa` resolves to the intended model/fallback and
|
|
70
|
-
its isolated child process can register the configured
|
|
71
|
-
|
|
79
|
+
1. `browser-qa` resolves to the intended model/fallback and its self-contained
|
|
80
|
+
private workflow, and its isolated child process can register the configured
|
|
81
|
+
Antigravity model.
|
|
82
|
+
2. Spawn args contain `--no-skills` and the mandatory private skill for this
|
|
72
83
|
profile; ordinary profiles retain existing skill discovery behavior.
|
|
73
84
|
3. Auth profile listing and all error output are redacted; model-authored input
|
|
74
85
|
cannot execute code in the credential-bearing process.
|
|
@@ -76,6 +87,26 @@ evidence. The role uses `antigravity/gemini-3-flash-preview`, falling back to
|
|
|
76
87
|
origins, path/mode hardening, private empty-template creation, explicit
|
|
77
88
|
credential requests, non-executable flows, and successful redacted evidence
|
|
78
89
|
creation.
|
|
79
|
-
5.
|
|
90
|
+
5. Browser QA flows/evidence live only inside the owning sub-agent directory;
|
|
91
|
+
deleting the run removes them while persistent auth config/state remains.
|
|
92
|
+
6. Completed test runs report clickable screenshot, video, and trace links
|
|
80
93
|
whenever those artifacts exist.
|
|
81
|
-
|
|
94
|
+
7. Suite tests/typecheck, host checks, and suite sync pass.
|
|
95
|
+
|
|
96
|
+
## Real-browser regression test
|
|
97
|
+
|
|
98
|
+
The repository includes a local mock-page E2E that launches real Chromium and
|
|
99
|
+
asserts PNG screenshots, WebM video, sanitized trace output, and absolute
|
|
100
|
+
path/`file:` URI metadata:
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
npx playwright install chromium
|
|
104
|
+
npm run test:browser-qa-e2e
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Normal suite tests keep this case skipped; the Publish workflow runs it on
|
|
108
|
+
Linux after installing Chromium. The runner writes into a temporary simulated
|
|
109
|
+
sub-agent directory. For manual inspection only, explicit E2E runs copy the
|
|
110
|
+
latest artifacts to `.pi/qa-runs/browser-qa-e2e/latest/` and print clickable
|
|
111
|
+
links; this test-only published copy is not the runtime storage contract. Set
|
|
112
|
+
`BROWSER_QA_KEEP_EVIDENCE=0` to skip that copy.
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
"smoke:tools": "PI_OFFLINE=1 pi --no-session -p \"ping\"",
|
|
23
23
|
"smoke": "npm run smoke:explicit && npm run smoke:auto && npm run smoke:tools",
|
|
24
24
|
"test": "bun test test",
|
|
25
|
+
"test:browser-qa-e2e": "BROWSER_QA_RUNNER_E2E=1 bun test test/async-subagents/browser-qa-runner.e2e.test.ts",
|
|
25
26
|
"test:async-subagents-e2e": "ASYNC_SUBAGENTS_E2E=1 ASYNC_SUBAGENTS_DEBUG_LOGS=1 ASYNC_SUBAGENTS_MODEL=zai/glm-5-turbo bun test --concurrent --max-concurrency=30 test/async-subagents",
|
|
26
27
|
"test:async-subagents-selection-e2e": "ASYNC_SUBAGENTS_SELECTION_E2E=1 ASYNC_SUBAGENTS_MODEL=zai/glm-5-turbo bun test --concurrent --max-concurrency=30 test/async-subagents/selection-e2e.test.ts",
|
|
27
28
|
"test:prompt-evals:tool-selection": "PROMPT_EVAL_E2E=1 bun test --concurrent --max-concurrency=10 test/tool-selection-e2e.test.ts",
|
|
@@ -381,7 +381,7 @@ export function resolveAgentTaskConfig(
|
|
|
381
381
|
return {
|
|
382
382
|
profile,
|
|
383
383
|
extraArgs,
|
|
384
|
-
isolatedSkills:
|
|
384
|
+
isolatedSkills: resolveIsolatedSkills(selectedType, profile),
|
|
385
385
|
fallbackModels,
|
|
386
386
|
retry: resolveRetryConfig(config.retry, profile?.retry),
|
|
387
387
|
maxResultBytes: profile?.maxResultBytes ?? config.maxResultBytes,
|
|
@@ -399,6 +399,12 @@ export function resolveAgentTaskConfig(
|
|
|
399
399
|
};
|
|
400
400
|
}
|
|
401
401
|
|
|
402
|
+
function resolveIsolatedSkills(selectedType: string | undefined, profile: SubagentTypeConfig | undefined): string[] {
|
|
403
|
+
const configured = arrayOfStrings(profile?.isolatedSkills) ?? [];
|
|
404
|
+
if (selectedType !== "browser-qa") return configured;
|
|
405
|
+
return [...new Set([getBrowserQaSkillPath(), ...configured])];
|
|
406
|
+
}
|
|
407
|
+
|
|
402
408
|
export function resolveSubagentRoutingConfig(config: SubagentConfig): ResolvedSubagentRoutingConfig {
|
|
403
409
|
return { ...DEFAULT_ROUTING_CONFIG, ...(config.routing ?? {}) };
|
|
404
410
|
}
|
|
@@ -24,6 +24,8 @@ export interface SpawnAgentOptions {
|
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
export const DEFAULT_AGENT_TIMEOUT_MS = 30 * 60 * 1000;
|
|
27
|
+
const BROWSER_QA_WORKSPACE_DIR = "browser-qa";
|
|
28
|
+
const SUBAGENT_AGENT_DIR_ENV = "PI_SUBAGENT_AGENT_DIR";
|
|
27
29
|
const AGENT_TIMEOUT_EXIT_CODE = 124;
|
|
28
30
|
const AGENT_TIMEOUT_KILL_GRACE_MS = 5_000;
|
|
29
31
|
const AGENT_SETTLED_TERMINATE_GRACE_MS = 50;
|
|
@@ -46,6 +48,7 @@ export function spawnAgent(
|
|
|
46
48
|
validateBasename(task.id, "task.id");
|
|
47
49
|
const agentDir = path.join(runDir, task.id);
|
|
48
50
|
fs.mkdirSync(agentDir, { recursive: true });
|
|
51
|
+
prepareBrowserQaWorkspace(agentDir, task.subagentType);
|
|
49
52
|
|
|
50
53
|
// Clean previous state when reusing a run directory/agent id.
|
|
51
54
|
for (const f of [
|
|
@@ -136,7 +139,7 @@ export function spawnAgent(
|
|
|
136
139
|
|
|
137
140
|
const proc = spawn(invocation.command, invocation.args, {
|
|
138
141
|
cwd,
|
|
139
|
-
env: subagentEnvironment(process.env),
|
|
142
|
+
env: subagentEnvironment(process.env, task.subagentType === "browser-qa" ? agentDir : undefined),
|
|
140
143
|
stdio: ["pipe", "pipe", "pipe"],
|
|
141
144
|
});
|
|
142
145
|
proc.stdin.on("error", (error: NodeJS.ErrnoException) => {
|
|
@@ -698,8 +701,8 @@ function getEnvModel(): string | undefined {
|
|
|
698
701
|
return trimmed ? trimmed : undefined;
|
|
699
702
|
}
|
|
700
703
|
|
|
701
|
-
function subagentEnvironment(env: NodeJS.ProcessEnv): NodeJS.ProcessEnv {
|
|
702
|
-
|
|
704
|
+
function subagentEnvironment(env: NodeJS.ProcessEnv, agentDir?: string): NodeJS.ProcessEnv {
|
|
705
|
+
const result: NodeJS.ProcessEnv = {
|
|
703
706
|
...env,
|
|
704
707
|
PI_MODEL_SUITABLE_TOOLS_PRESERVE_SELECTION: "1",
|
|
705
708
|
PI_TERMINAL_BELL_DISABLED: "1",
|
|
@@ -709,6 +712,21 @@ function subagentEnvironment(env: NodeJS.ProcessEnv): NodeJS.ProcessEnv {
|
|
|
709
712
|
"question",
|
|
710
713
|
]),
|
|
711
714
|
};
|
|
715
|
+
delete result[SUBAGENT_AGENT_DIR_ENV];
|
|
716
|
+
if (agentDir) result[SUBAGENT_AGENT_DIR_ENV] = fs.realpathSync(agentDir);
|
|
717
|
+
return result;
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
function prepareBrowserQaWorkspace(agentDir: string, subagentType: string | undefined): void {
|
|
721
|
+
const workspace = path.join(agentDir, BROWSER_QA_WORKSPACE_DIR);
|
|
722
|
+
fs.rmSync(workspace, { recursive: true, force: true });
|
|
723
|
+
if (subagentType !== "browser-qa") return;
|
|
724
|
+
const flows = path.join(workspace, "flows");
|
|
725
|
+
fs.mkdirSync(flows, { recursive: true, mode: 0o700 });
|
|
726
|
+
if (process.platform !== "win32") {
|
|
727
|
+
fs.chmodSync(workspace, 0o700);
|
|
728
|
+
fs.chmodSync(flows, 0o700);
|
|
729
|
+
}
|
|
712
730
|
}
|
|
713
731
|
|
|
714
732
|
function appendEnvList(value: string | undefined, items: readonly string[]): string {
|
|
@@ -1,31 +1,71 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: browser-qa-private
|
|
3
|
-
description: Private workflow for deterministic browser bug reproduction and fix verification with redacted project auth and Playwright evidence.
|
|
3
|
+
description: Private self-contained workflow for deterministic browser bug reproduction and fix verification with redacted project auth and Playwright evidence.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Browser QA
|
|
7
7
|
|
|
8
|
-
Use
|
|
9
|
-
|
|
8
|
+
Use this skill's bundled runner as the only browser interface. It already owns
|
|
9
|
+
Playwright, browser/context lifecycle, tracing, video, screenshots, origin
|
|
10
|
+
isolation, authentication, redaction, and cleanup. Do not invoke another browser
|
|
11
|
+
CLI, create shared/default browser sessions, or generate executable browser code.
|
|
12
|
+
|
|
13
|
+
Never read, print, grep, copy, or edit credential values from
|
|
14
|
+
`.pi/qa_auth.jsonc` yourself.
|
|
10
15
|
|
|
11
16
|
## Workflow
|
|
12
17
|
|
|
13
18
|
1. Resolve `scripts/browser-qa-runner.mjs` relative to this skill.
|
|
14
|
-
2.
|
|
19
|
+
2. Use the launcher-provided `$PI_SUBAGENT_AGENT_DIR/browser-qa/` workspace.
|
|
20
|
+
The launcher creates its private `flows/` directory and the runner rejects
|
|
21
|
+
flows or evidence destinations outside this owning sub-agent directory. Do
|
|
22
|
+
not override `PI_SUBAGENT_AGENT_DIR` or copy evidence to shared project paths.
|
|
23
|
+
3. Discover the requested target, expected behavior, and the smallest scenario
|
|
24
|
+
that can prove it. If the target cannot be reached or started, report the
|
|
25
|
+
concrete blocker instead of substituting static checks for browser QA.
|
|
26
|
+
4. Run `node <runner> profiles`. Choose a profile only when the task names its
|
|
15
27
|
id or safe profile traits make the choice unambiguous. Otherwise stop with
|
|
16
28
|
`QA_PROFILE_REQUIRED` and list only ids, descriptions, and traits.
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
29
|
+
5. Inspect the target code and write a declarative JSONC flow under
|
|
30
|
+
`$PI_SUBAGENT_AGENT_DIR/browser-qa/flows/`. Never put credentials or
|
|
31
|
+
executable JavaScript in it.
|
|
32
|
+
6. Run:
|
|
20
33
|
`node <runner> run --profile <id> --base-url <url> --flow <flow.jsonc>`.
|
|
21
34
|
Profile id, URL, and flow path are non-secret; never pass credentials as
|
|
22
35
|
arguments or environment variables.
|
|
23
|
-
|
|
36
|
+
7. Report deterministic assertions and every artifact returned by the runner.
|
|
24
37
|
For each screenshot, video, or trace, emit a separate clickable Markdown
|
|
25
38
|
link using its `uri` and also show its absolute `path`. Do this for failed
|
|
26
39
|
runs too whenever `artifacts` is present; never report only `evidenceDir`.
|
|
27
40
|
Visual inspection supplements assertions; it does not replace them.
|
|
28
41
|
|
|
42
|
+
## Scenario design
|
|
43
|
+
|
|
44
|
+
- Define the expected postcondition before writing interactions. A successful
|
|
45
|
+
click or navigation is not proof; assert the resulting URL, text, value,
|
|
46
|
+
count, visibility, enabled state, or checked state.
|
|
47
|
+
- Keep the flow minimal and reproducible. Capture setup, the action under test,
|
|
48
|
+
and at least one observable outcome; add a screenshot at the state that best
|
|
49
|
+
explains the result.
|
|
50
|
+
- Prefer stable user-facing locators in this order: `testId`; semantic `role`
|
|
51
|
+
plus accessible `name`; `label`; `placeholder`; visible `text`; CSS only as a
|
|
52
|
+
last resort. Use `exact: true` when similar elements could make a match
|
|
53
|
+
ambiguous.
|
|
54
|
+
- Let locator actions auto-wait. Use `waitFor` for an explicit UI state and use
|
|
55
|
+
`waitForTimeout` only for a short, unavoidable animation/debounce—not as a
|
|
56
|
+
substitute for an assertion. Set flow `timeoutMs` only as high as the target
|
|
57
|
+
legitimately needs.
|
|
58
|
+
- Place `authRejectedIf` immediately after navigation or any transition that
|
|
59
|
+
may reveal expired authentication.
|
|
60
|
+
- Never weaken an assertion merely to make a failing run pass. If the observed
|
|
61
|
+
product behavior differs from the expectation, preserve the failure evidence
|
|
62
|
+
and report the mismatch.
|
|
63
|
+
|
|
64
|
+
Read `references/qa-design.md` when designing a non-trivial flow, diagnosing an
|
|
65
|
+
ambiguous failure, or deciding what evidence proves the result.
|
|
66
|
+
|
|
67
|
+
## Flow contract
|
|
68
|
+
|
|
29
69
|
The flow is `{ "steps": [...] }` with at most 100 steps. Supported actions:
|
|
30
70
|
|
|
31
71
|
- navigation: `goto`, `reload`, `waitFor`, `waitForTimeout`
|
|
@@ -50,16 +90,21 @@ assertions require exactly one of `equals` or `includes`.
|
|
|
50
90
|
"locator": { "role": "heading", "name": "Settings", "exact": true }
|
|
51
91
|
},
|
|
52
92
|
{ "action": "click", "locator": { "testId": "save-settings" } },
|
|
53
|
-
{ "action": "assertText", "locator": { "testId": "toast" }, "includes": "Saved" }
|
|
93
|
+
{ "action": "assertText", "locator": { "testId": "toast" }, "includes": "Saved" },
|
|
94
|
+
{ "action": "screenshot", "name": "settings-saved" }
|
|
54
95
|
]
|
|
55
96
|
}
|
|
56
97
|
```
|
|
57
98
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
99
|
+
For multiple profiles, invoke the runner separately. Every invocation gets an
|
|
100
|
+
isolated browser context and exclusive evidence directory; the runner closes
|
|
101
|
+
all owned browser resources on success and failure. Flows, screenshots, video,
|
|
102
|
+
sanitized traces, and runner result manifests remain under
|
|
103
|
+
`$PI_SUBAGENT_AGENT_DIR/browser-qa/` so normal sub-agent shutdown or cleanup
|
|
104
|
+
deletes them with the run directory. `.pi/qa_auth.jsonc` and reusable
|
|
105
|
+
`.pi/qa-auth-state/` remain project-local and persistent.
|
|
106
|
+
|
|
107
|
+
## Credentials and blocked runs
|
|
63
108
|
|
|
64
109
|
If the runner returns `QA_AUTH_UPDATE_REQUIRED`, stop and explicitly report that
|
|
65
110
|
browser QA requires credentials or an auth-config update. Ask the user to fill
|
|
@@ -68,9 +113,13 @@ empty template was created at that path. Relay only the runner's profile, file,
|
|
|
68
113
|
reason, action, and template-created state; never read the generated file or
|
|
69
114
|
attempt to recover by exposing or replaying credentials.
|
|
70
115
|
|
|
116
|
+
For any other blocked run, report the runner status and redacted reason. Do not
|
|
117
|
+
claim that browser QA passed based on source inspection, unit tests, or a build.
|
|
118
|
+
|
|
71
119
|
After any runner invocation that actually performed browser testing, include
|
|
72
120
|
all non-empty `artifacts.screenshots`, `artifacts.videos`, and
|
|
73
121
|
`artifacts.traces` groups in the final response. These links are mandatory so
|
|
74
122
|
the user can open the evidence directly.
|
|
75
123
|
|
|
76
|
-
See `references/qa-auth.example.jsonc
|
|
124
|
+
See `references/qa-auth.example.jsonc`, `references/qa-flow.example.jsonc`, and
|
|
125
|
+
`references/qa-design.md`.
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# Designing deterministic browser QA flows
|
|
2
|
+
|
|
3
|
+
Use this reference with the bundled declarative runner. It intentionally does
|
|
4
|
+
not describe a separate browser CLI or executable Playwright scripts.
|
|
5
|
+
|
|
6
|
+
## Build the proof before the steps
|
|
7
|
+
|
|
8
|
+
Write down three things first:
|
|
9
|
+
|
|
10
|
+
1. **Setup:** the page and state needed to expose the behavior.
|
|
11
|
+
2. **Action:** the smallest user interaction that exercises it.
|
|
12
|
+
3. **Oracle:** the observable state that proves success or reproduces failure.
|
|
13
|
+
|
|
14
|
+
Good oracles are product-visible and specific: an exact URL, a stable status
|
|
15
|
+
message, a field value, item count, enabled/disabled state, or checked state.
|
|
16
|
+
Avoid treating “the click did not throw” or “the screenshot looks plausible” as
|
|
17
|
+
proof.
|
|
18
|
+
|
|
19
|
+
When verifying a fix, prefer a focused regression flow over a broad tour of the
|
|
20
|
+
application. If multiple independent states matter, assert each one explicitly.
|
|
21
|
+
|
|
22
|
+
## Choose resilient locators
|
|
23
|
+
|
|
24
|
+
Prefer locators that match how users and accessibility APIs identify controls:
|
|
25
|
+
|
|
26
|
+
1. `testId` when the product exposes a stable test contract.
|
|
27
|
+
2. `role` with accessible `name` for buttons, links, headings, dialogs, and
|
|
28
|
+
similar semantic elements.
|
|
29
|
+
3. `label` for form controls.
|
|
30
|
+
4. `placeholder` or visible `text` when they are stable product copy.
|
|
31
|
+
5. `css` only when no semantic contract exists.
|
|
32
|
+
|
|
33
|
+
Use `exact: true` when duplicate or substring matches are possible. Avoid CSS
|
|
34
|
+
that encodes DOM depth, generated classes, styling details, or element order.
|
|
35
|
+
If a locator is ambiguous, inspect nearby source or rendered copy and choose a
|
|
36
|
+
more specific product contract rather than adding arbitrary delays.
|
|
37
|
+
|
|
38
|
+
## Wait for state, not time
|
|
39
|
+
|
|
40
|
+
Runner interactions inherit Playwright auto-waiting. Usually an action followed
|
|
41
|
+
by an assertion is enough. Use `waitFor` only when the next operation depends on
|
|
42
|
+
a distinct attached/detached/visible/hidden transition.
|
|
43
|
+
|
|
44
|
+
`waitForTimeout` is bounded to five seconds and should be exceptional—for a
|
|
45
|
+
known animation, debounce, or externally scheduled transition with no
|
|
46
|
+
observable intermediate state. Sleeping longer hides races instead of proving
|
|
47
|
+
behavior. If a normal operation legitimately needs more time, adjust the flow's
|
|
48
|
+
`timeoutMs` rather than inserting repeated sleeps.
|
|
49
|
+
|
|
50
|
+
## Authentication transitions
|
|
51
|
+
|
|
52
|
+
Add `authRejectedIf` directly after initial navigation and after transitions
|
|
53
|
+
that can redirect to login or display an expired-session marker. This converts
|
|
54
|
+
stale credentials into an explicit update request instead of misreporting a
|
|
55
|
+
product regression.
|
|
56
|
+
|
|
57
|
+
Do not encode credentials, tokens, storage values, or login form secrets in the
|
|
58
|
+
flow. The trusted runner applies the selected profile internally and removes
|
|
59
|
+
secret-bearing evidence if it detects disclosure.
|
|
60
|
+
|
|
61
|
+
## Evidence strategy
|
|
62
|
+
|
|
63
|
+
The runner always attempts a final or failure screenshot, records video, and
|
|
64
|
+
creates a sanitized trace once the browser launches. Add named `screenshot`
|
|
65
|
+
steps only at states that materially help explain the result—for example before
|
|
66
|
+
and after a destructive interaction, or when a transient success message is
|
|
67
|
+
the oracle.
|
|
68
|
+
|
|
69
|
+
Use evidence by purpose:
|
|
70
|
+
|
|
71
|
+
- **Screenshot:** quick review of one meaningful visual state.
|
|
72
|
+
- **Video:** chronological confirmation of the complete user flow.
|
|
73
|
+
- **Trace:** action/DOM timing diagnosis for a failed or flaky interaction.
|
|
74
|
+
|
|
75
|
+
Assertions determine pass/fail; evidence explains it. Preserve and link every
|
|
76
|
+
artifact group returned on both passed and failed runs.
|
|
77
|
+
|
|
78
|
+
## Diagnose failures without weakening the test
|
|
79
|
+
|
|
80
|
+
Classify the first failing step:
|
|
81
|
+
|
|
82
|
+
- wrong target/setup or service unavailable;
|
|
83
|
+
- authentication rejected or expired;
|
|
84
|
+
- locator no longer matches the product contract;
|
|
85
|
+
- expected state never appeared;
|
|
86
|
+
- actual product behavior contradicts the expectation.
|
|
87
|
+
|
|
88
|
+
Fix the flow only when its setup or locator is wrong. Do not replace a precise
|
|
89
|
+
assertion with a vague one, increase timeouts reflexively, or remove the failing
|
|
90
|
+
step to manufacture a pass. Keep the failure artifacts and state the expected
|
|
91
|
+
versus observed behavior.
|
|
92
|
+
|
|
93
|
+
## Cleanup and isolation
|
|
94
|
+
|
|
95
|
+
Each runner invocation owns one isolated context and evidence directory and
|
|
96
|
+
closes its browser resources in a `finally` path. Do not create parallel shared
|
|
97
|
+
or default sessions outside the runner. Test multiple auth profiles with
|
|
98
|
+
separate invocations so cookies, storage, traces, and evidence cannot mix.
|
|
99
|
+
|
|
100
|
+
Keep the declarative flow and every generated screenshot, video, trace, and
|
|
101
|
+
result manifest inside `$PI_SUBAGENT_AGENT_DIR/browser-qa/`. The launcher owns
|
|
102
|
+
that path and the runner validates it before opening a browser. Do not override
|
|
103
|
+
the environment path or copy evidence into shared `.pi/qa-runs`/`.pi/qa-flows`
|
|
104
|
+
directories: the agent-local workspace is intentionally removed by the normal
|
|
105
|
+
sub-agent shutdown and cleanup lifecycle. Authentication config and reusable
|
|
106
|
+
auth state are separate persistent inputs and stay under project `.pi/`.
|
|
@@ -10,7 +10,9 @@ import { strFromU8, strToU8, unzipSync, zipSync } from "../vendor/fflate.mjs";
|
|
|
10
10
|
|
|
11
11
|
const CONFIG_RELATIVE = ".pi/qa_auth.jsonc";
|
|
12
12
|
const STATE_RELATIVE = path.join(".pi", "qa-auth-state");
|
|
13
|
-
const
|
|
13
|
+
const SUBAGENT_AGENT_DIR_ENV = "PI_SUBAGENT_AGENT_DIR";
|
|
14
|
+
const QA_WORKSPACE_RELATIVE = "browser-qa";
|
|
15
|
+
const EVIDENCE_RELATIVE = "evidence";
|
|
14
16
|
const EXIT_AUTH_UPDATE_REQUIRED = 42;
|
|
15
17
|
const EXIT_PROFILE_REQUIRED = 43;
|
|
16
18
|
const PROFILE_ID = /^[A-Za-z0-9._-]+$/;
|
|
@@ -80,23 +82,25 @@ async function main() {
|
|
|
80
82
|
const profile = selected.profile;
|
|
81
83
|
const secrets = collectSecrets(profile.auth);
|
|
82
84
|
try {
|
|
83
|
-
|
|
85
|
+
const agentDir = resolveBrowserQaAgentDirectory(cwd, process.env[SUBAGENT_AGENT_DIR_ENV]);
|
|
86
|
+
await runQa({ cwd, agentDir, args, profileId, profile });
|
|
84
87
|
} catch (error) {
|
|
85
88
|
if (error instanceof QaStatusError) throw error;
|
|
86
89
|
throw new QaStatusError("QA_RUN_FAILED", redact(safeReason(error), secrets), 1, profileId);
|
|
87
90
|
}
|
|
88
91
|
}
|
|
89
92
|
|
|
90
|
-
async function runQa({ cwd, args, profileId, profile }) {
|
|
93
|
+
async function runQa({ cwd, agentDir, args, profileId, profile }) {
|
|
91
94
|
if (!args.flow) throw new QaStatusError("QA_RUN_FAILED", "--flow is required", 1, profileId);
|
|
92
|
-
const
|
|
95
|
+
const workspaceDir = path.join(agentDir, QA_WORKSPACE_RELATIVE);
|
|
96
|
+
const flowPath = resolveExistingPrivateFile(workspaceDir, args.flow, "QA flow", false);
|
|
93
97
|
const flow = readFlow(flowPath, profileId);
|
|
94
98
|
const allowedOrigins = normalizeAllowedOrigins(profile.allowedOrigins, profileId);
|
|
95
99
|
const baseURL = normalizeBaseUrl(args.baseUrl ?? profile.baseUrl ?? allowedOrigins[0], allowedOrigins, profileId);
|
|
96
100
|
validateAuthConfiguration(cwd, profile.auth, allowedOrigins, profileId);
|
|
97
101
|
const runId = safeRunId(args.runId ?? `${timestamp()}-${profileId}`);
|
|
98
|
-
const evidenceDir = path.join(
|
|
99
|
-
createExclusivePrivateDirectory(
|
|
102
|
+
const evidenceDir = path.join(workspaceDir, EVIDENCE_RELATIVE, runId, profileId);
|
|
103
|
+
createExclusivePrivateDirectory(agentDir, evidenceDir);
|
|
100
104
|
|
|
101
105
|
const playwright = loadPlaywright(cwd);
|
|
102
106
|
const browser = await playwright.chromium.launch({ headless: true });
|
|
@@ -779,6 +783,45 @@ function isAllowedUrl(raw, allowedOrigins) {
|
|
|
779
783
|
}
|
|
780
784
|
}
|
|
781
785
|
|
|
786
|
+
function resolveBrowserQaAgentDirectory(cwd, value) {
|
|
787
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
788
|
+
throw new Error(`${SUBAGENT_AGENT_DIR_ENV} is required for browser QA runs`);
|
|
789
|
+
}
|
|
790
|
+
const projectRoot = fs.realpathSync(cwd);
|
|
791
|
+
const subagentRoot = path.join(projectRoot, ".pi", "subagents");
|
|
792
|
+
const resolved = path.resolve(value);
|
|
793
|
+
if (!fs.existsSync(resolved)) throw new Error("browser QA agent directory is missing");
|
|
794
|
+
const real = fs.realpathSync(resolved);
|
|
795
|
+
if (!isInside(subagentRoot, real)) throw new Error("browser QA agent directory must be inside .pi/subagents");
|
|
796
|
+
assertNoSymlinkComponents(projectRoot, real, "browser QA agent directory");
|
|
797
|
+
if (!fs.statSync(real).isDirectory()) throw new Error("browser QA agent directory must be a real directory");
|
|
798
|
+
|
|
799
|
+
const promptFile = path.join(real, "prompt.md");
|
|
800
|
+
const projectFile = path.join(real, "project_cwd");
|
|
801
|
+
const typeFile = path.join(real, "subagent_type");
|
|
802
|
+
for (const [file, label] of [[promptFile, "prompt"], [projectFile, "project metadata"], [typeFile, "type metadata"]]) {
|
|
803
|
+
if (!fs.existsSync(file) || !fs.statSync(file).isFile()) throw new Error(`browser QA ${label} is missing`);
|
|
804
|
+
assertNoSymlinkComponents(real, file, `browser QA ${label}`);
|
|
805
|
+
}
|
|
806
|
+
const recordedProject = fs.readFileSync(projectFile, "utf8").trim();
|
|
807
|
+
if (!recordedProject || fs.realpathSync(recordedProject) !== projectRoot) {
|
|
808
|
+
throw new Error("browser QA agent directory belongs to another project");
|
|
809
|
+
}
|
|
810
|
+
if (fs.readFileSync(typeFile, "utf8").trim() !== "browser-qa") {
|
|
811
|
+
throw new Error("browser QA runner requires a browser-qa sub-agent directory");
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
const workspace = path.join(real, QA_WORKSPACE_RELATIVE);
|
|
815
|
+
if (!fs.existsSync(workspace)) throw new Error("browser QA workspace is missing");
|
|
816
|
+
assertNoSymlinkComponents(real, workspace, "browser QA workspace");
|
|
817
|
+
const workspaceStat = fs.statSync(workspace);
|
|
818
|
+
if (!workspaceStat.isDirectory()) throw new Error("browser QA workspace must be a real directory");
|
|
819
|
+
if (process.platform !== "win32" && (workspaceStat.mode & 0o077) !== 0) {
|
|
820
|
+
throw new Error("browser QA workspace must use private directory permissions (0700)");
|
|
821
|
+
}
|
|
822
|
+
return real;
|
|
823
|
+
}
|
|
824
|
+
|
|
782
825
|
function resolveExistingPrivateFile(cwd, value, label, requirePrivate = true) {
|
|
783
826
|
if (typeof value !== "string" || value.length === 0) throw new Error(`${label} path is missing`);
|
|
784
827
|
const root = fs.realpathSync(cwd);
|
|
@@ -794,9 +837,13 @@ function resolveExistingPrivateFile(cwd, value, label, requirePrivate = true) {
|
|
|
794
837
|
return real;
|
|
795
838
|
}
|
|
796
839
|
|
|
797
|
-
function
|
|
840
|
+
function isInside(root, target) {
|
|
798
841
|
const relative = path.relative(root, target);
|
|
799
|
-
|
|
842
|
+
return !relative || (!relative.startsWith("..") && !path.isAbsolute(relative));
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
function assertInside(root, target, label) {
|
|
846
|
+
if (isInside(root, target)) return;
|
|
800
847
|
throw new Error(`${label} must be project-local`);
|
|
801
848
|
}
|
|
802
849
|
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { TaskDetails } from "../tool/types.js";
|
|
2
2
|
import { EMPTY_STATE, type TaskState } from "./state.js";
|
|
3
3
|
|
|
4
|
+
export const TODO_STATE_ENTRY_TYPE = "pi-tools-suite:todo-state";
|
|
5
|
+
|
|
4
6
|
/**
|
|
5
7
|
* Discriminator for `details` envelopes that match the persisted `TaskDetails`
|
|
6
8
|
* shape. Defensive — branch entries from older or corrupt sessions are
|
|
@@ -12,10 +14,29 @@ export function isTaskDetails(value: unknown): value is TaskDetails {
|
|
|
12
14
|
return Array.isArray(v.tasks) && typeof v.nextId === "number";
|
|
13
15
|
}
|
|
14
16
|
|
|
17
|
+
function taskDetailsFromEntry(entry: unknown): TaskDetails | undefined {
|
|
18
|
+
const candidate = entry as {
|
|
19
|
+
type?: string;
|
|
20
|
+
customType?: string;
|
|
21
|
+
data?: unknown;
|
|
22
|
+
message?: { role?: string; toolName?: string; details?: unknown };
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
if (candidate.type === "custom" && candidate.customType === TODO_STATE_ENTRY_TYPE) {
|
|
26
|
+
return isTaskDetails(candidate.data) ? candidate.data : undefined;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const message = candidate.message;
|
|
30
|
+
if (candidate.type !== "message" || message?.role !== "toolResult" || message.toolName !== "todo") return undefined;
|
|
31
|
+
return isTaskDetails(message.details) ? message.details : undefined;
|
|
32
|
+
}
|
|
33
|
+
|
|
15
34
|
/**
|
|
16
|
-
* Walk the current branch in chronological order; the LAST
|
|
17
|
-
*
|
|
18
|
-
* (last-write-wins).
|
|
35
|
+
* Walk the current branch in chronological order; the LAST valid snapshot
|
|
36
|
+
* from either a `todo` tool result or a todo custom state entry wins
|
|
37
|
+
* (last-write-wins). Custom entries persist slash-command mutations, which do
|
|
38
|
+
* not otherwise produce tool results. When no matching entry exists, returns
|
|
39
|
+
* `EMPTY_STATE`.
|
|
19
40
|
*
|
|
20
41
|
* Pure of module state — `index.ts` writes the returned snapshot into the
|
|
21
42
|
* store after this returns. The function explicitly does NOT touch the store
|
|
@@ -24,14 +45,11 @@ export function isTaskDetails(value: unknown): value is TaskDetails {
|
|
|
24
45
|
export function replayFromBranch(ctx: { sessionManager: { getBranch(): Iterable<unknown> } }): TaskState {
|
|
25
46
|
let result: TaskState = { tasks: [...EMPTY_STATE.tasks], nextId: EMPTY_STATE.nextId };
|
|
26
47
|
for (const entry of ctx.sessionManager.getBranch()) {
|
|
27
|
-
const
|
|
28
|
-
if (
|
|
29
|
-
const msg = e.message;
|
|
30
|
-
if (!msg || msg.role !== "toolResult" || msg.toolName !== "todo") continue;
|
|
31
|
-
if (!isTaskDetails(msg.details)) continue;
|
|
48
|
+
const details = taskDetailsFromEntry(entry);
|
|
49
|
+
if (!details) continue;
|
|
32
50
|
result = {
|
|
33
|
-
tasks:
|
|
34
|
-
nextId:
|
|
51
|
+
tasks: details.tasks.map((t) => ({ ...t })),
|
|
52
|
+
nextId: details.nextId,
|
|
35
53
|
};
|
|
36
54
|
}
|
|
37
55
|
return result;
|
|
@@ -23,7 +23,7 @@ import {
|
|
|
23
23
|
syncPersistedPlan,
|
|
24
24
|
} from "./state/persistence.js";
|
|
25
25
|
import { AUTO_CLEAR_COMPLETED_MESSAGE, autoClearCompletedTodos } from "./state/auto-clear.js";
|
|
26
|
-
import { replayFromBranch } from "./state/replay.js";
|
|
26
|
+
import { replayFromBranch, TODO_STATE_ENTRY_TYPE } from "./state/replay.js";
|
|
27
27
|
import { isTaskBlocked, selectTasksByStatus, selectTodoCounts } from "./state/selectors.js";
|
|
28
28
|
import { applyTaskMutation } from "./state/state-reducer.js";
|
|
29
29
|
import { activateStateScope, commitState, getState, replaceState } from "./state/store.js";
|
|
@@ -98,6 +98,7 @@ interface TodoToolRegistrationOptions extends TodoToolHooks {
|
|
|
98
98
|
|
|
99
99
|
type TodoStateEventContext = { sessionManager?: { getSessionFile?: () => unknown; getSessionId?: () => unknown } };
|
|
100
100
|
type TodoStateEventEmitter = { events?: { emit?: (channel: string, data: unknown) => void } };
|
|
101
|
+
type TodoStateEntryWriter = { appendEntry<T = unknown>(customType: string, data?: T): void };
|
|
101
102
|
|
|
102
103
|
interface TodosCommandOptions {
|
|
103
104
|
status?: TaskStatus;
|
|
@@ -238,6 +239,20 @@ export function publishTodoState(
|
|
|
238
239
|
});
|
|
239
240
|
}
|
|
240
241
|
|
|
242
|
+
function appendTodoStateSnapshot(
|
|
243
|
+
pi: TodoStateEntryWriter,
|
|
244
|
+
action: TaskAction,
|
|
245
|
+
params: Record<string, unknown>,
|
|
246
|
+
): void {
|
|
247
|
+
const state = getState();
|
|
248
|
+
pi.appendEntry(TODO_STATE_ENTRY_TYPE, {
|
|
249
|
+
action,
|
|
250
|
+
params,
|
|
251
|
+
tasks: state.tasks,
|
|
252
|
+
nextId: state.nextId,
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
|
|
241
256
|
function handlePersistCommand(args: unknown, ctx: { cwd?: string; hasUI?: boolean; ui?: { notify?: (message: string, level?: NotifyLevel) => void } }): boolean {
|
|
242
257
|
const [command, subcommand = "status"] = getCommandTokens(args);
|
|
243
258
|
if (command !== "persist") return false;
|
|
@@ -310,12 +325,13 @@ function handleScopeCommand(
|
|
|
310
325
|
}
|
|
311
326
|
|
|
312
327
|
function clearTodos(
|
|
313
|
-
pi: TodoStateEventEmitter,
|
|
328
|
+
pi: TodoStateEventEmitter & TodoStateEntryWriter,
|
|
314
329
|
ctx: { cwd?: string; hasUI?: boolean; ui?: { notify?: (message: string, level?: NotifyLevel) => void } },
|
|
315
330
|
): void {
|
|
316
331
|
const result = applyTaskMutation(getState(), "clear", { action: "clear" });
|
|
317
332
|
if (result.op.kind !== "clear") return;
|
|
318
333
|
commitState(result.state);
|
|
334
|
+
appendTodoStateSnapshot(pi, "clear", { action: "clear" });
|
|
319
335
|
publishTodoState(pi, ctx, "clear", { action: "clear" });
|
|
320
336
|
const sync = syncPersistedPlan(ctx.cwd, result.state);
|
|
321
337
|
const persistedText = sync?.completed ? `\nProject todo plan removed: ${sync.path}` : "";
|
|
@@ -449,9 +465,12 @@ export function registerTodosCommand(pi: ExtensionAPI): void {
|
|
|
449
465
|
handler: async (args, ctx) => {
|
|
450
466
|
activateTodoStateScope(ctx);
|
|
451
467
|
if (handlePersistCommand(args, ctx)) return;
|
|
452
|
-
if (handleScopeCommand(args, ctx, () =>
|
|
468
|
+
if (handleScopeCommand(args, ctx, () => {
|
|
469
|
+
appendTodoStateSnapshot(pi, "update", { command: "scope" });
|
|
470
|
+
publishTodoState(pi as TodoStateEventEmitter, ctx);
|
|
471
|
+
})) return;
|
|
453
472
|
if (getCommandTokens(args)[0] === "clear") {
|
|
454
|
-
clearTodos(pi
|
|
473
|
+
clearTodos(pi, ctx);
|
|
455
474
|
return;
|
|
456
475
|
}
|
|
457
476
|
if (!ctx.hasUI) {
|
|
@@ -529,7 +548,10 @@ export function registerTodosCommand(pi: ExtensionAPI): void {
|
|
|
529
548
|
description: "Select todo ids to continue from a persisted plan; pending/in_progress items outside the scope become deferred.",
|
|
530
549
|
handler: async (args, ctx) => {
|
|
531
550
|
activateTodoStateScope(ctx);
|
|
532
|
-
handleScopeCommand(`scope ${getCommandText(args)}`, ctx, () =>
|
|
551
|
+
handleScopeCommand(`scope ${getCommandText(args)}`, ctx, () => {
|
|
552
|
+
appendTodoStateSnapshot(pi, "update", { command: "scope" });
|
|
553
|
+
publishTodoState(pi as TodoStateEventEmitter, ctx);
|
|
554
|
+
});
|
|
533
555
|
},
|
|
534
556
|
});
|
|
535
557
|
|
|
@@ -537,7 +559,7 @@ export function registerTodosCommand(pi: ExtensionAPI): void {
|
|
|
537
559
|
description: "Clear all todos on the current branch and remove the persisted project plan if enabled.",
|
|
538
560
|
handler: async (_args, ctx) => {
|
|
539
561
|
activateTodoStateScope(ctx);
|
|
540
|
-
clearTodos(pi
|
|
562
|
+
clearTodos(pi, ctx);
|
|
541
563
|
},
|
|
542
564
|
});
|
|
543
565
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-ui-extend",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.21",
|
|
4
4
|
"description": "Pix: a workspace-first terminal UI for Pi with tabs, readable tool activity, voice input, and bundled agent tools.",
|
|
5
5
|
"private": false,
|
|
6
6
|
"repository": {
|
|
@@ -58,6 +58,7 @@
|
|
|
58
58
|
"test": "mise exec node@24.16.0 -- npm run test:inner",
|
|
59
59
|
"test:inner": "node --import tsx --test \"tests/**/*.test.ts\"",
|
|
60
60
|
"test:tools-suite": "npm --prefix external/pi-tools-suite test",
|
|
61
|
+
"test:browser-qa-e2e": "npm --prefix external/pi-tools-suite run test:browser-qa-e2e",
|
|
61
62
|
"sync:pi-tools-suite": "node scripts/sync-pi-tools-suite.mjs",
|
|
62
63
|
"sync:pi-tools-suite:check": "node scripts/sync-pi-tools-suite.mjs --check",
|
|
63
64
|
"sync:sdk-pin": "node scripts/sync-sdk-pin.mjs",
|
|
@@ -86,6 +87,7 @@
|
|
|
86
87
|
},
|
|
87
88
|
"devDependencies": {
|
|
88
89
|
"@types/node": "24.12.4",
|
|
90
|
+
"playwright": "1.62.1",
|
|
89
91
|
"tsx": "4.22.1",
|
|
90
92
|
"typescript": "5.9.3"
|
|
91
93
|
},
|