@qaping/cli 0.1.0
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/bin/qaping +233 -0
- package/docs/QA-PLAN-FORMAT.md +71 -0
- package/package.json +32 -0
- package/skill/qaping/SKILL.md +112 -0
package/bin/qaping
ADDED
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// qaping — automatic QA for game developers, as a thin brand wrapper over the
|
|
3
|
+
// pingfusi kit (a real dependency; same accounts, credits, and service under
|
|
4
|
+
// the hood). Every command resolves the installed kit and drives it with the
|
|
5
|
+
// qaping wrapper options — MCP mount /api/mcp/qaping, MCP server key "qaping",
|
|
6
|
+
// the qaping skill + always-loaded rule — so the customer only ever sees
|
|
7
|
+
// qaping, and the kit's own pingfusi instruction surfaces are neither
|
|
8
|
+
// installed nor swept (the vendored installer's --skip-instruction-surfaces
|
|
9
|
+
// contract).
|
|
10
|
+
//
|
|
11
|
+
// qaping setup [claude-code|cursor|codex] [--force] one-command onboarding
|
|
12
|
+
// qaping remove|uninstall [--client <c>] sweep ONLY qaping's state
|
|
13
|
+
// qaping publish-build <game.zip> --platform windows|macos
|
|
14
|
+
// qaping wait <ping_id> [--timeout <seconds>]
|
|
15
|
+
// qaping whoami | version | help
|
|
16
|
+
"use strict";
|
|
17
|
+
const fs = require("fs");
|
|
18
|
+
const os = require("os");
|
|
19
|
+
const path = require("path");
|
|
20
|
+
const readline = require("readline");
|
|
21
|
+
const { spawnSync } = require("child_process");
|
|
22
|
+
|
|
23
|
+
const QPKG = path.resolve(__dirname, "..");
|
|
24
|
+
|
|
25
|
+
// The kit is a normal npm dependency when qaping is installed, and a workspace
|
|
26
|
+
// sibling in the monorepo checkout (where node_modules may not be linked yet)
|
|
27
|
+
// — resolve the package, never a hardcoded install path.
|
|
28
|
+
function resolveKitDir() {
|
|
29
|
+
try {
|
|
30
|
+
return path.dirname(require.resolve("pingfusi/package.json", { paths: [QPKG] }));
|
|
31
|
+
} catch {
|
|
32
|
+
const sibling = path.join(QPKG, "..", "kit");
|
|
33
|
+
try {
|
|
34
|
+
if (require(path.join(sibling, "package.json")).name === "pingfusi") return sibling;
|
|
35
|
+
} catch { /* fall through */ }
|
|
36
|
+
throw new Error("cannot resolve the pingfusi kit (a dependency of qaping) — reinstall: npm i -g qaping");
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// The always-loaded rule (the qaping counterpart of the installer's RULE_BODY):
|
|
41
|
+
// short, WHEN to reach for qaping; the skill owns the full loop. agent-setup
|
|
42
|
+
// writes it to ~/.claude/rules/qaping.md and ~/.cursor/rules/qaping.mdc.
|
|
43
|
+
const RULE_BODY = `This machine has qaping: automatic QA for games — real human playtesters play the developer's builds on their own hardware and report back, and the coding agent runs the whole loop.
|
|
44
|
+
Whenever the user asks to set up QA for a game, run QA on a patch, check whether a change broke anything, or playtest a game or build, load the qaping skill and follow it — it owns authoring/maintaining QA-PLAN.md in the game repo and the per-patch run.
|
|
45
|
+
Builds ship with \`qaping publish-build <game.zip> --platform windows|macos\`; the printed /b/<slug> URL is what a round is filed against (store-delivered games file with their Steam/TestFlight/App Store URL instead).
|
|
46
|
+
The MCP tools are qaping_playtest (file a round with real human playtesters), qaping_results (fetch a finished round, free) and qaping_wait (continue a pending round — whenever a filing or wait returns pending, call qaping_wait again immediately; never report pending as the answer).
|
|
47
|
+
Playtests are duration-billed at 2 credits per minute of play per playtester — state the estimated cost before filing. Windows rounds return a recording + questionnaire and no transcript; never promise one.
|
|
48
|
+
`;
|
|
49
|
+
|
|
50
|
+
// The wrapper object harness/setup.js's opts.wrapper API takes; also the one
|
|
51
|
+
// source for the vendored installer's brand flags (spawned per command below).
|
|
52
|
+
function qapingWrapper() {
|
|
53
|
+
return {
|
|
54
|
+
brand: "qaping",
|
|
55
|
+
// npm registry name ≠ bin name: the similarity rule blocks bare "qaping"
|
|
56
|
+
// (petition open, QAPING_PLAN §8), so the global install pulls @qaping/cli.
|
|
57
|
+
installPackage: "@qaping/cli",
|
|
58
|
+
appUrl: process.env.QAPING_APP_URL || undefined,
|
|
59
|
+
mcpPath: "/api/mcp/qaping",
|
|
60
|
+
serverKey: "qaping",
|
|
61
|
+
skipInstructionSurfaces: true,
|
|
62
|
+
skillRoot: path.join(QPKG, "skill"),
|
|
63
|
+
ruleAsset: { fileBaseName: "qaping", body: RULE_BODY },
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function vendorFlags() {
|
|
68
|
+
const w = qapingWrapper();
|
|
69
|
+
return [
|
|
70
|
+
...(w.appUrl ? ["--app-url", w.appUrl] : []),
|
|
71
|
+
"--mcp-path", w.mcpPath,
|
|
72
|
+
"--server-key", w.serverKey,
|
|
73
|
+
"--skip-instruction-surfaces",
|
|
74
|
+
];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// One vendored-installer invocation per command, brand flags AFTER the
|
|
78
|
+
// command/user args (the installer parses flags positionally-agnostic, but
|
|
79
|
+
// `wait` takes the first non-flag arg as the ping id — user args stay first).
|
|
80
|
+
function spawnVendor(args) {
|
|
81
|
+
const vendor = path.join(resolveKitDir(), "vendor", "pingfusi-review.mjs");
|
|
82
|
+
const r = spawnSync(process.execPath, [vendor, ...args, ...vendorFlags()], { stdio: "inherit" });
|
|
83
|
+
return r.status == null ? 1 : r.status;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Running from the monorepo checkout (not an npm install): the repo root's
|
|
87
|
+
// .git two levels up, and this file not under any node_modules. The plain
|
|
88
|
+
// "..git two up" test alone would also match node_modules/qaping inside a
|
|
89
|
+
// user's git project — that is an install, not a checkout.
|
|
90
|
+
function isSourceCheckout() {
|
|
91
|
+
return !/node_modules/.test(QPKG) && fs.existsSync(path.join(QPKG, "..", "..", ".git"));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Same io shape harness/setup.js's defaultIO builds (it isn't exported; the
|
|
95
|
+
// kit-only steps a wrapper brand skips mean this needs no probe machinery
|
|
96
|
+
// beyond the contract). npm/which route through the kit's proc.js — it owns
|
|
97
|
+
// the Windows npm.cmd/where split.
|
|
98
|
+
function defaultIO(kitDir) {
|
|
99
|
+
const { spawnNpmSync, whichSync } = require(path.join(kitDir, "harness", "proc.js"));
|
|
100
|
+
return {
|
|
101
|
+
isTTY: !!process.stdin.isTTY,
|
|
102
|
+
log: (...a) => console.log(...a),
|
|
103
|
+
run: (cmd, args) => (cmd === "npm"
|
|
104
|
+
? spawnNpmSync(args, { stdio: "inherit" })
|
|
105
|
+
: spawnSync(cmd, args, { stdio: "inherit" })),
|
|
106
|
+
probe: (cmd, args) => {
|
|
107
|
+
try {
|
|
108
|
+
const r = spawnSync(cmd, args, { stdio: "pipe", timeout: 10_000 });
|
|
109
|
+
return !r.error && (r.status === 0 || !!((r.stdout && r.stdout.length) || (r.stderr && r.stderr.length)));
|
|
110
|
+
} catch { return false; }
|
|
111
|
+
},
|
|
112
|
+
which: (cmd) => whichSync(cmd),
|
|
113
|
+
ask: (q) =>
|
|
114
|
+
new Promise((res) => {
|
|
115
|
+
if (!process.stdin.isTTY) return res("");
|
|
116
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
117
|
+
rl.question(q, (a) => { rl.close(); res(a.trim().toLowerCase()); });
|
|
118
|
+
}),
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const VALID_CLIENTS = ["claude-desktop", "claude-code", "cursor", "codex"];
|
|
123
|
+
|
|
124
|
+
async function cmdSetup(argv) {
|
|
125
|
+
const kitDir = resolveKitDir();
|
|
126
|
+
const { setup } = require(path.join(kitDir, "harness", "setup.js"));
|
|
127
|
+
const { resolveToken } = require(path.join(kitDir, "harness", "review-qa.js"));
|
|
128
|
+
// accepts both the positional form (`setup cursor`) and `setup --client cursor`
|
|
129
|
+
const args = argv.filter((a) => a !== "--force");
|
|
130
|
+
const client = ((args[1] === "--client" ? args[2] : args[1]) || "").toLowerCase();
|
|
131
|
+
const r = await setup(defaultIO(kitDir), {
|
|
132
|
+
home: os.homedir(),
|
|
133
|
+
sourceCheckout: isSourceCheckout(),
|
|
134
|
+
resolveToken,
|
|
135
|
+
force: argv.includes("--force"),
|
|
136
|
+
dittoApiKey: false,
|
|
137
|
+
mcpClient: VALID_CLIENTS.includes(client) ? client : null,
|
|
138
|
+
wrapper: qapingWrapper(),
|
|
139
|
+
});
|
|
140
|
+
if (r.ok) {
|
|
141
|
+
console.log(`
|
|
142
|
+
Ask your agent:
|
|
143
|
+
"Set up QA for my game." (authors QA-PLAN.md in your repo)
|
|
144
|
+
"Run QA on this patch." (builds, ships, files playtest rounds)
|
|
145
|
+
"Did this patch break anything?" (the same loop, asked plainly)
|
|
146
|
+
|
|
147
|
+
Playtest rounds are answered by real human playtesters on their own hardware;
|
|
148
|
+
your agent files them, reads the results, and reports back.
|
|
149
|
+
(re-run anytime: qaping setup)`);
|
|
150
|
+
}
|
|
151
|
+
process.exit(r.ok ? 0 : 1);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function cmdRemove(argv) {
|
|
155
|
+
const kitDir = resolveKitDir();
|
|
156
|
+
// The kit bin's client-scope parser: --client validation is a hard error
|
|
157
|
+
// BEFORE anything destructive runs, never a scope widening.
|
|
158
|
+
const { kitSkillClient } = require(path.join(kitDir, "bin", "pingfusi"));
|
|
159
|
+
let client;
|
|
160
|
+
try { client = kitSkillClient(argv); }
|
|
161
|
+
catch (e) { console.error(`✗ ${e.message}`); process.exit(2); }
|
|
162
|
+
if (client !== "claude-desktop") { // desktop has MCP config but no coding-agent skills
|
|
163
|
+
try {
|
|
164
|
+
const w = qapingWrapper();
|
|
165
|
+
const removed = require(path.join(kitDir, "harness", "agent-setup.js"))
|
|
166
|
+
.removeSkills(os.homedir(), client, { skillRoot: w.skillRoot, ruleAsset: w.ruleAsset });
|
|
167
|
+
if (removed.length) console.log(`✓ Removed qaping agent skill(s): ${removed.join(", ")}`);
|
|
168
|
+
} catch { /* best-effort — the installer's own removal still runs */ }
|
|
169
|
+
}
|
|
170
|
+
// The vendored remove with qaping's flags sweeps ONLY the qaping MCP entries
|
|
171
|
+
// and never touches the shared ~/.config/pingfusi login or pingfusi's own
|
|
172
|
+
// rule/skill files (--skip-instruction-surfaces + a non-pingfusi server key).
|
|
173
|
+
process.exit(spawnVendor(argv));
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function cmdPublishBuild(argv) {
|
|
177
|
+
// Kit passthrough: harness/publish-build.js main() takes argv AFTER the
|
|
178
|
+
// command (argv[0] = the zip path) and owns its own exit codes. The brand
|
|
179
|
+
// seam keeps every printed command/tool name qaping's own: usage says
|
|
180
|
+
// `qaping publish-build`, and the next step names qaping_playtest — the
|
|
181
|
+
// tool the /api/mcp/qaping mount actually registers.
|
|
182
|
+
return require(path.join(resolveKitDir(), "harness", "publish-build.js"))
|
|
183
|
+
.main(argv.slice(1), { brandCommand: "qaping publish-build", nextStepToolName: "qaping_playtest" });
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const HELP = `qaping — automatic QA for your game: real human playtesters, driven by your coding agent
|
|
187
|
+
|
|
188
|
+
usage:
|
|
189
|
+
qaping setup [claude-code|cursor|codex] [--force]
|
|
190
|
+
qaping remove [--client <c>] remove qaping's MCP entries, skill and rule
|
|
191
|
+
qaping publish-build <game.zip> --platform windows|macos
|
|
192
|
+
qaping wait <ping_id> [--timeout <seconds>]
|
|
193
|
+
qaping whoami
|
|
194
|
+
qaping version
|
|
195
|
+
|
|
196
|
+
Then ask your agent: "Set up QA for my game." or "Run QA on this patch."`;
|
|
197
|
+
|
|
198
|
+
function route(cmd) {
|
|
199
|
+
if (cmd === "version" || cmd === "--version" || cmd === "-v") return "version";
|
|
200
|
+
if (cmd === "help" || cmd === "--help" || cmd === "-h") return "help";
|
|
201
|
+
if (cmd === "setup") return "setup";
|
|
202
|
+
if (cmd === "remove" || cmd === "uninstall") return "remove";
|
|
203
|
+
if (cmd === "wait" || cmd === "whoami") return "vendor";
|
|
204
|
+
if (cmd === "publish-build") return "publish-build";
|
|
205
|
+
return "unknown";
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function main() {
|
|
209
|
+
const argv = process.argv.slice(2);
|
|
210
|
+
try {
|
|
211
|
+
switch (route(argv[0])) {
|
|
212
|
+
case "version": return console.log(require("../package.json").version);
|
|
213
|
+
case "help": return console.log(HELP);
|
|
214
|
+
case "setup": return void cmdSetup(argv).catch((e) => { console.error(`✗ ${(e && e.message) || e}`); process.exit(1); });
|
|
215
|
+
case "remove": return cmdRemove(argv);
|
|
216
|
+
// `wait` also names the wait tool the qaping mount registers — the
|
|
217
|
+
// vendored default (the stock mount's wait) is not on /api/mcp/qaping.
|
|
218
|
+
case "vendor": return process.exit(spawnVendor(argv[0] === "wait" ? [...argv, "--wait-tool", "qaping_wait"] : argv));
|
|
219
|
+
case "publish-build": return void cmdPublishBuild(argv);
|
|
220
|
+
default:
|
|
221
|
+
console.error(HELP);
|
|
222
|
+
process.exit(1);
|
|
223
|
+
}
|
|
224
|
+
} catch (e) {
|
|
225
|
+
// a clean message for sync failures (e.g. the kit dependency missing) —
|
|
226
|
+
// never a raw stack at the user
|
|
227
|
+
console.error(`✗ ${(e && e.message) || e}`);
|
|
228
|
+
process.exit(1);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (require.main === module) main();
|
|
233
|
+
module.exports = { route, resolveKitDir, qapingWrapper, vendorFlags, isSourceCheckout, RULE_BODY, HELP };
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# QA-PLAN.md — the format
|
|
2
|
+
|
|
3
|
+
`QA-PLAN.md` lives at the GAME repo's root. The agent authors and maintains it;
|
|
4
|
+
the developer owns it and approves every commit. Agents parse it by reading —
|
|
5
|
+
keep it exactly this shape, nothing fancier.
|
|
6
|
+
|
|
7
|
+
## Plan frontmatter
|
|
8
|
+
|
|
9
|
+
YAML at the very top of the file:
|
|
10
|
+
|
|
11
|
+
```yaml
|
|
12
|
+
---
|
|
13
|
+
game: Solar Drift
|
|
14
|
+
build_command: ./scripts/package.sh --release # the ONE command that produces a shippable build
|
|
15
|
+
platforms: [windows] # windows | macos | ios
|
|
16
|
+
input: XInput gamepad # optional; default keyboard+mouse
|
|
17
|
+
---
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
`build_command` is recorded once at setup so every future run can build without
|
|
21
|
+
asking. `input` declares the hardware the first-15-minutes check (and any check
|
|
22
|
+
that needs it) is played on — rounds must ask playtesters to confirm what they
|
|
23
|
+
actually used.
|
|
24
|
+
|
|
25
|
+
## Checks
|
|
26
|
+
|
|
27
|
+
One `##` section per check; the heading is the check's short name. The section
|
|
28
|
+
opens with a fenced yaml block, then the steps as prose:
|
|
29
|
+
|
|
30
|
+
```yaml
|
|
31
|
+
id: save-loads # stable slug — never reused, never renamed
|
|
32
|
+
rung: human # human | code
|
|
33
|
+
origin: authored # "authored", or the round id that minted/last changed it
|
|
34
|
+
last_verified: 2026-08-25 # date or build tag of the last passing verification
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
- **Steps are prose for a person**: numbered, imperative, written so a
|
|
38
|
+
playtester who has never seen the game can follow them — they become the
|
|
39
|
+
round's instructions verbatim.
|
|
40
|
+
- **Order the file critical-path first**: boots to menu, previous-version save
|
|
41
|
+
loads, first 15 minutes on the declared input hardware, core loop, settings
|
|
42
|
+
persist — then game-specific checks.
|
|
43
|
+
- A `rung: code` check keeps its prose (it documents intent) and adds one
|
|
44
|
+
`test:` line naming the repo test that now asserts it, e.g.
|
|
45
|
+
`` test: `tests/save_compat.test.ts` ``.
|
|
46
|
+
|
|
47
|
+
## Example section
|
|
48
|
+
|
|
49
|
+
````markdown
|
|
50
|
+
## A previous-version save loads
|
|
51
|
+
|
|
52
|
+
```yaml
|
|
53
|
+
id: save-loads
|
|
54
|
+
rung: human
|
|
55
|
+
origin: authored
|
|
56
|
+
last_verified: 2026-08-25
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
1. From the main menu, choose Continue.
|
|
60
|
+
2. Load the provided save "campaign-mid.sav" (made on the previous release).
|
|
61
|
+
3. Confirm the game resumes in the desert outpost with the inventory intact.
|
|
62
|
+
4. Play for one minute; note anything missing, corrupted, or visually wrong.
|
|
63
|
+
````
|
|
64
|
+
|
|
65
|
+
## Maintenance rules
|
|
66
|
+
|
|
67
|
+
- Every run updates the run checks' `last_verified`; findings-driven edits
|
|
68
|
+
record the round id as `origin`.
|
|
69
|
+
- New checks start `rung: human`, `origin: authored`.
|
|
70
|
+
- Promotion (human → code) and any deletion/demotion happen only with the
|
|
71
|
+
developer's explicit agreement.
|
package/package.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@qaping/cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "qaping: automatic QA for your game. Your coding agent authors and maintains the QA plan, ships each patch's build to real human playtesters, and reports what broke.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"qaping",
|
|
7
|
+
"game-qa",
|
|
8
|
+
"playtest",
|
|
9
|
+
"regression",
|
|
10
|
+
"qa",
|
|
11
|
+
"agent"
|
|
12
|
+
],
|
|
13
|
+
"license": "MIT",
|
|
14
|
+
"type": "commonjs",
|
|
15
|
+
"bin": {
|
|
16
|
+
"qaping": "bin/qaping"
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"bin/",
|
|
20
|
+
"skill/",
|
|
21
|
+
"docs/"
|
|
22
|
+
],
|
|
23
|
+
"engines": {
|
|
24
|
+
"node": "^20.17.0 || ^22.13.0 || >=23.5.0"
|
|
25
|
+
},
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"pingfusi": "^0.16.0"
|
|
28
|
+
},
|
|
29
|
+
"publishConfig": {
|
|
30
|
+
"access": "public"
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: qaping
|
|
3
|
+
description: Run automatic QA for a game repo through qaping — real human playtesters play the developer's builds on their own hardware, and the agent runs the whole loop. Use when the user says "set up QA for my game", "run QA on this patch", "did this patch break anything", "playtest my game" or "playtest this build", or asks for regression checks, patch verification, or human playtesting of a game. Covers both halves of the product - authoring and maintaining QA-PLAN.md in the game repo, and the per-patch run (read the diff, build, publish-build, file qaping_playtest rounds, report on the PR, file issues, promote settled checks into repo code tests). Do not use for web apps, websites, or UI review of anything that is not a game build — the qaping tools test game builds only.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# qaping — the QA loop for a game repo
|
|
7
|
+
|
|
8
|
+
Two jobs, one file. SETUP authors `QA-PLAN.md` — the committed list of checks
|
|
9
|
+
this game must pass. RUN executes it per patch: pick the affected checks, build,
|
|
10
|
+
ship the build to real human playtesters, report what they found. Over patches,
|
|
11
|
+
checks migrate from human rounds to repo code tests, so QA gets cheaper. All
|
|
12
|
+
service contact goes through three MCP tools — `qaping_playtest`,
|
|
13
|
+
`qaping_results`, `qaping_wait` — plus `qaping publish-build` for hosting
|
|
14
|
+
the build.
|
|
15
|
+
|
|
16
|
+
## Ground rules (read before filing anything)
|
|
17
|
+
|
|
18
|
+
- **Playtesters are real people.** A session takes real minutes to be claimed
|
|
19
|
+
and played. File, then `qaping_wait`; whenever a filing or wait returns
|
|
20
|
+
pending, immediately call `qaping_wait` again with the same ping_id. Never
|
|
21
|
+
report pending as the final answer, never file a duplicate round.
|
|
22
|
+
- **Costs are duration-billed**: 2 credits per minute of play, per playtester
|
|
23
|
+
(`est_minutes × 2 × players`). State the estimate to the user BEFORE filing.
|
|
24
|
+
- **Windows rounds return a recording + questionnaire and NO transcript**
|
|
25
|
+
(`transcript_status:'unavailable'`) — never promise one; plan to watch the
|
|
26
|
+
recording for timestamps. macOS and iOS rounds include an inline [mm:ss]
|
|
27
|
+
think-aloud transcript.
|
|
28
|
+
- **Machine replay is not self-serve yet.** Never claim an automated replay ran.
|
|
29
|
+
The only automated rung you can run today is a `rung: code` test in the
|
|
30
|
+
dev's own repo/CI.
|
|
31
|
+
- **QA-PLAN.md is the dev's file.** Commit it only with their approval; never
|
|
32
|
+
delete or demote a human check without them agreeing.
|
|
33
|
+
|
|
34
|
+
## SETUP — author QA-PLAN.md (first run in a repo)
|
|
35
|
+
|
|
36
|
+
1. Read the game repo: engine, how a shippable build is produced, save system,
|
|
37
|
+
settings, input devices the game supports.
|
|
38
|
+
2. Ask the dev ONCE for their build command (the one command that produces a
|
|
39
|
+
shippable build) and which platforms they ship; record both in the plan's
|
|
40
|
+
frontmatter so every future run can build without asking again.
|
|
41
|
+
3. Author `QA-PLAN.md` at the repo root, format per `docs/QA-PLAN-FORMAT.md`
|
|
42
|
+
(shipped with this package — read it first). Order checks critical-path
|
|
43
|
+
first, then game-specific ones:
|
|
44
|
+
1. the game boots to the main menu
|
|
45
|
+
2. a previous-version save loads
|
|
46
|
+
3. the first 15 minutes play clean on the declared input hardware
|
|
47
|
+
4. the core loop works (name it concretely — the thing this game is)
|
|
48
|
+
5. settings persist across a restart
|
|
49
|
+
Every check carries `{id, rung, origin, last_verified}`. New checks start
|
|
50
|
+
`rung: human`, `origin: authored`.
|
|
51
|
+
4. Tell the dev how runs get triggered, honestly: for now the loop runs from
|
|
52
|
+
their own machine or agent — after a patch lands, they say "run QA on this
|
|
53
|
+
patch" and the RUN half below executes. Do NOT offer to wire a CI workflow
|
|
54
|
+
file: a plain CI runner has no qaping MCP connection and no login, so a
|
|
55
|
+
committed workflow could not run this loop today. A CI-triggered recipe is
|
|
56
|
+
a documented follow-up; until it ships, per-patch QA is an ask-your-agent
|
|
57
|
+
step.
|
|
58
|
+
5. Show the dev the plan and commit only on their approval.
|
|
59
|
+
|
|
60
|
+
## RUN — per patch
|
|
61
|
+
|
|
62
|
+
1. **Select.** Read the diff (or PR). Pick the checks it can plausibly affect,
|
|
63
|
+
plus the always-run criticals (the critical-path block above). Tell the user
|
|
64
|
+
which checks run and why.
|
|
65
|
+
2. **Build** via the recorded build command; zip the result.
|
|
66
|
+
3. **Ship:** `qaping publish-build <game.zip> --platform windows|macos` →
|
|
67
|
+
prints a `/b/<slug>` URL (temporary hosting; filing a playtest extends the
|
|
68
|
+
build through the round; each publish mints a NEW URL). A store-delivered
|
|
69
|
+
game files with its Steam store page, TestFlight public link, or App Store
|
|
70
|
+
page as `url` instead.
|
|
71
|
+
4. **File** ONE `qaping_playtest` per batch of human checks a single session
|
|
72
|
+
can cover, `est_minutes` an honest sum of the steps — the service accepts
|
|
73
|
+
5–30 minutes (10 is the standard session; outside that range the filing is
|
|
74
|
+
refused, not clamped), and the clock is play time: download/install happens
|
|
75
|
+
before it starts. The authored
|
|
76
|
+
`instructions` + `steps` must embed:
|
|
77
|
+
- the selected checks' steps, in play order (the plan's prose, verbatim
|
|
78
|
+
where possible);
|
|
79
|
+
- the previous round's findings as context — "last round reported X —
|
|
80
|
+
recheck" — so regressions get re-examined on every patch;
|
|
81
|
+
- the hardware-verification convention: when the game needs more than
|
|
82
|
+
keyboard+mouse, say so up front in `instructions` (playtesters self-select
|
|
83
|
+
before claiming) AND add an explicit step asking which input device was
|
|
84
|
+
actually used, with options — the answer comes back verified in
|
|
85
|
+
`steps_result`.
|
|
86
|
+
For a regression batch that knows exactly what it is testing, send
|
|
87
|
+
`questionnaire:'none'` so your steps are the only questions; leave the
|
|
88
|
+
standard questionnaire on for a first baseline round.
|
|
89
|
+
5. **Wait:** `qaping_wait` until results arrive (see ground rules).
|
|
90
|
+
6. **Report** from `qaping_results`:
|
|
91
|
+
- PR comment via `gh pr comment`: ONE verdict sentence first, then one line
|
|
92
|
+
per check — ✅/❌ with a recording timestamp ([mm:ss] from the transcript
|
|
93
|
+
where present; from watching the recording on Windows) — then credits
|
|
94
|
+
spent.
|
|
95
|
+
- One `gh issue create` per CONFIRMED bug: title, repro steps rebuilt from
|
|
96
|
+
the playtester's answers, a timestamp link into the recording. Issues are
|
|
97
|
+
drafts for the dev's own triage — never near-duplicates, never one issue
|
|
98
|
+
per symptom of the same bug.
|
|
99
|
+
- Update `QA-PLAN.md`: each run check's `last_verified` becomes this
|
|
100
|
+
build/date; a check changed because of this round records the round id as
|
|
101
|
+
`origin`.
|
|
102
|
+
|
|
103
|
+
## PROMOTE — make the suite cheaper
|
|
104
|
+
|
|
105
|
+
When a check has passed and is mechanically assertable — a save file loads, a
|
|
106
|
+
config parses, an asset path resolves — offer to write a normal test in the
|
|
107
|
+
repo's own test suite. On the dev's yes: write the test, flip the check's
|
|
108
|
+
`rung` to `code`, keep the prose (it documents intent) and add a `test:` line
|
|
109
|
+
naming the test file. `rung: code` checks then run in the dev's own CI for
|
|
110
|
+
free — that is the point: every check starts human and gets cheaper over time.
|
|
111
|
+
Checks about feel, difficulty, or ambiguous visuals stay `rung: human`
|
|
112
|
+
permanently. Never delete or demote a human check without the dev agreeing.
|