qaify-playwright-reporter 0.7.0 → 0.9.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/README.md +22 -7
- package/dist/audit-api.d.ts +118 -0
- package/dist/audit-api.js +628 -0
- package/dist/audit-static.d.ts +15 -6
- package/dist/audit-static.js +101 -80
- package/dist/audit.d.ts +4 -0
- package/dist/audit.js +4 -1
- package/dist/cli.js +21 -4
- package/dist/fix.d.ts +25 -1
- package/dist/fix.js +112 -14
- package/package.json +1 -1
- package/templates/qaify-audit.yml +27 -9
- package/templates/qaify-fix.yml +12 -3
package/README.md
CHANGED
|
@@ -105,11 +105,22 @@ Untuk aplikasi yang repo FE dan BE-nya terpisah dan belum memasang Playwright, Q
|
|
|
105
105
|
|
|
106
106
|
1. Pilih repo runner: repo kosong (mis. `qaify-runner`) atau repo aplikasinya sendiri. Jalankan `npx qaify-playwright-reporter init-runner` di dalamnya. Perintah ini menulis `.github/workflows/qaify-audit.yml`.
|
|
107
107
|
2. Pasang GitHub App Qaify di repo runner, repo FE, dan repo BE.
|
|
108
|
-
3. Di repo runner, isi secret `QA_FACTORY_TOKEN` (token reporter repo runner), dan variable `QA_FACTORY_URL`. Secret `CLAUDE_CODE_OAUTH_TOKEN` bersifat opsional.
|
|
108
|
+
3. Di repo runner, isi secret `QA_FACTORY_TOKEN` (token reporter repo runner), dan variable `QA_FACTORY_URL`. Secret `CLAUDE_CODE_OAUTH_TOKEN` bersifat opsional. Secret ini dibutuhkan untuk [perbaikan temuan oleh Claude](#perbaikan-temuan-audit-oleh-claude-pr-ke-repo-febe) dan audit mode Claude.
|
|
109
109
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
-
|
|
110
|
+
Mode audit dipilih di Qaify: **Tanpa AI** (default) atau **Claude**, sebagai setelan aplikasi, dan bisa diganti di dialog **Audit sekarang**. Jadi Claude bisa dipakai hanya untuk memperbaiki temuan, sementara auditnya tetap tanpa AI. Mode Claude tanpa secret di atas dijalankan sebagai Tanpa AI (dengan peringatan di run).
|
|
111
|
+
|
|
112
|
+
- **Tanpa AI** (`qaify audit-static`): build, test, `composer audit` / `npm audit` (satu temuan per paket, paling banyak 10 di laporan), pemeriksaan API BE, cek kontrak FE↔BE, dan smoke test Playwright FE (desktop + mobile, screenshot setiap halaman untuk lampiran laporan PDF). Smoke test juga memeriksa elemen yang meluap, gambar rusak, tombol yang tertutup elemen lain, error JavaScript, request API, dan aset yang gagal. Setiap jenis masalah difoto lagi dengan kotak merah di elemennya atau banner berisi pesan errornya. Temuan smoke diberi halaman dan berkas sumber bila bisa ditebak (route FE → komponen halaman, route BE → method controller). Request FE yang gagal dipetakan ke controller BE, dan route BE yang error dihubungkan ke halaman FE yang memanggilnya.
|
|
113
|
+
|
|
114
|
+
Pemeriksaan API (deterministik, aturan bisnis tidak ditebak):
|
|
115
|
+
- **Login API** dengan akun di catatan aplikasi (`email / password`; token Bearer atau cookie Sanctum SPA). Beri label untuk uji antar-role, mis. `admin: admin@pos.test / rahasia; kasir: kasir@pos.test / kasir123`.
|
|
116
|
+
- **Smoke API**: semua route GET (yang terlindungi juga, bila login berhasil), `{param}` diisi ID pertama dari route index-nya. Respons ≥500 menjadi temuan.
|
|
117
|
+
- **Validasi**: POST/PUT/PATCH dikirimi body kosong dan nilai yang melanggar aturan FormRequest / `validate()`. 500, atau input salah yang diterima, menjadi temuan.
|
|
118
|
+
- **Otorisasi**: route API pengubah data tanpa middleware auth, route GET publik yang mengembalikan data di resource yang lainnya terlindungi, dan route `role:x` yang bisa dibuka akun berlabel role lain.
|
|
119
|
+
- **Kontrak FE↔BE**: `axios`/`fetch` di `src/` FE dicocokkan dengan `route:list` BE (path tidak ada / method tidak diterima).
|
|
120
|
+
- **Claude**: Claude yang mengaudit. Hasilnya lebih dalam, termasuk logika bisnis dan kontrak API, tetapi memakan kuota langganan.
|
|
121
|
+
- Variable `QA_FACTORY_AUDIT_MODE` (`static` / `claude`) hanya dipakai bila backend Qaify belum mengirim mode (versi lama).
|
|
122
|
+
|
|
123
|
+
Runner yang dibuat sebelum 0.8 perlu memperbarui workflow: `npx qaify-playwright-reporter init-runner --force`. Tanpa itu, workflow lama tetap memilih mode dari secret dan variable. Runner yang dibuat sebelum 0.9 perlu menjalankan perintah yang sama agar pemeriksaan API dan perbaikan dependensi tanpa AI (`qaify-fix.yml`) aktif.
|
|
113
124
|
4. Di Qaify → **Aplikasi**, tambahkan aplikasi (repo FE, BE, runner), lalu klik **Audit sekarang**.
|
|
114
125
|
|
|
115
126
|
Alur di workflow:
|
|
@@ -128,7 +139,7 @@ Keamanan:
|
|
|
128
139
|
- `QA_FACTORY_TOKEN` tidak tersedia di step Claude.
|
|
129
140
|
- Isi repo diperlakukan sebagai data, bukan instruksi.
|
|
130
141
|
|
|
131
|
-
Satu audit adalah satu sesi Claude yang panjang (sampai 45 menit), sehingga memakan kuota langganan pemilik runner.
|
|
142
|
+
Satu audit mode Claude adalah satu sesi Claude yang panjang (sampai 45 menit), sehingga memakan kuota langganan pemilik runner. Audit tanpa AI tidak memakai kuota.
|
|
132
143
|
|
|
133
144
|
## Perbaikan temuan audit oleh Claude (PR ke repo FE/BE)
|
|
134
145
|
|
|
@@ -148,6 +159,8 @@ Model dipilih Qaify per temuan dan dikirim lewat konteks. Kamu tidak perlu mengi
|
|
|
148
159
|
|
|
149
160
|
Tier bisa diubah per aplikasi, model bisa dikunci, dan setiap temuan bisa diperbaiki dengan model pilihan (alias atau ID versi, mis. `claude-sonnet-5-5`). Perbaikan otomatis yang gagal dicoba sekali lagi dengan model satu tingkat di atasnya.
|
|
150
161
|
|
|
162
|
+
Advisory dependensi (`composer audit` / `npm audit`, berkas `composer.lock` / `package-lock.json`) tidak dikirim ke Claude. Qaify memakai "model" `qaify-deps`: step Claude dilewati, dan `qaify fix-deps` memperbarui paketnya di lockfile (`composer update <paket> --with-all-dependencies --no-install`, atau `npm update <paket> --package-lock-only` lalu `npm audit fix --package-lock-only`), mengecek ulang dengan audit, lalu membuka PR berisi lockfile. Tidak memakai kuota Claude. Bila versi perbaikannya di luar constraint `composer.json` / `package.json` (mis. perlu naik versi major), perbaikan ditandai gagal dengan alasan itu.
|
|
163
|
+
|
|
151
164
|
Alur di workflow:
|
|
152
165
|
|
|
153
166
|
```bash
|
|
@@ -155,7 +168,9 @@ Alur di workflow:
|
|
|
155
168
|
qaify fix-context --fix <id> --dir "$WORK" # clone repo target di commit yang diaudit (token read-only, lalu dicabut)
|
|
156
169
|
qaify fix-schema --out schema.json --instructions instructions.txt
|
|
157
170
|
claude -p "$(cat .qaify/prompt.txt)" --model "$MODEL" … # ubah kode + jalankan test, tanpa commit
|
|
158
|
-
|
|
171
|
+
# atau, untuk advisory dependensi (output mode=deps dari fix-context):
|
|
172
|
+
qaify fix-deps --dir "$WORK" # update lockfile tanpa AI → .qaify/claude.json
|
|
173
|
+
qaify fix-diff --dir "$WORK" --out "$WORK/out" # changes.patch (.env & lockfile dibuang, kecuali lockfile mode deps) → artefak
|
|
159
174
|
# job submit: VM bersih
|
|
160
175
|
qaify fix-submit --fix <id> --results claude.json --patch changes.patch --dir "$RUNNER_TEMP/push"
|
|
161
176
|
```
|
|
@@ -163,7 +178,7 @@ qaify fix-submit --fix <id> --results claude.json --patch changes.patch --dir "$
|
|
|
163
178
|
Keamanan:
|
|
164
179
|
- Job Claude tidak pernah memegang token push maupun `QA_FACTORY_TOKEN`.
|
|
165
180
|
- Job submit berjalan di VM baru. Job ini clone ulang dan menerapkan patch dengan `git apply`, sehingga hook dan `.git/config` dari job Claude tidak ikut terbawa. Token push (`contents:write`) hanya berlaku untuk satu repo, sekali pakai, dan dicabut setelah push.
|
|
166
|
-
- Patch ditolak bila menyentuh `.github/` atau `.env`, atau melebihi 20 berkas / 600 baris.
|
|
181
|
+
- Patch ditolak bila menyentuh `.github/` atau `.env`, atau melebihi 20 berkas / 600 baris (baris lockfile tidak dihitung).
|
|
167
182
|
|
|
168
183
|
## Uji lokal tanpa GitHub Actions
|
|
169
184
|
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import type { AuditFinding } from './audit.js';
|
|
2
|
+
import { type SourceLocation } from './audit-static.js';
|
|
3
|
+
export interface Route {
|
|
4
|
+
methods: string[];
|
|
5
|
+
uri: string;
|
|
6
|
+
action: string | null;
|
|
7
|
+
middleware: string[];
|
|
8
|
+
}
|
|
9
|
+
export interface Account {
|
|
10
|
+
label: string | null;
|
|
11
|
+
email: string;
|
|
12
|
+
password: string;
|
|
13
|
+
}
|
|
14
|
+
export interface Session {
|
|
15
|
+
label: string | null;
|
|
16
|
+
headers: Record<string, string>;
|
|
17
|
+
cookies: Map<string, string>;
|
|
18
|
+
}
|
|
19
|
+
export interface ApiProbe {
|
|
20
|
+
base: string;
|
|
21
|
+
fetch: typeof fetch;
|
|
22
|
+
routes: Route[];
|
|
23
|
+
/** Root repo BE, untuk berkas controller / FormRequest. */
|
|
24
|
+
root: string;
|
|
25
|
+
/** Sesi akun pertama yang berhasil login; null = hanya route publik. */
|
|
26
|
+
session: Session | null;
|
|
27
|
+
sessions: Session[];
|
|
28
|
+
/** "<akun> <uri index>" → ID pertama, untuk mengisi {param}. */
|
|
29
|
+
ids: Map<string, string | number | null>;
|
|
30
|
+
}
|
|
31
|
+
export interface CheckResult {
|
|
32
|
+
findings: AuditFinding[];
|
|
33
|
+
notes: string[];
|
|
34
|
+
}
|
|
35
|
+
/** `route:list --json` → route; HEAD dibuang dari method. */
|
|
36
|
+
export declare function parseRoutes(output: string): Route[];
|
|
37
|
+
export declare const isProtected: (route: Route) => boolean;
|
|
38
|
+
/** Role yang boleh mengakses route dari middleware role:admin|manager, CheckRole:admin, isAdmin; null = tanpa batas role. */
|
|
39
|
+
export declare function allowedRoles(route: Route): string[] | null;
|
|
40
|
+
/** Akun dari catatan aplikasi, mis. "admin: admin@pos.test / rahasia; kasir: kasir@pos.test / kasir123". */
|
|
41
|
+
export declare function parseLogins(instructions: string): Account[];
|
|
42
|
+
/** Token API di respons login (Sanctum / Passport / JWT), di kedalaman mana pun. */
|
|
43
|
+
export declare function findToken(json: unknown, depth?: number): string | null;
|
|
44
|
+
/** Login API: token Bearer dari respons, atau sesi cookie Sanctum SPA (csrf-cookie + X-XSRF-TOKEN). */
|
|
45
|
+
export declare function apiLogin(probe: ApiProbe, account: Account): Promise<Session | null>;
|
|
46
|
+
/** Login semua akun; akun pertama yang berhasil jadi sesi utama. */
|
|
47
|
+
export declare function openApi(routes: Route[], accounts: Account[], opts: {
|
|
48
|
+
base: string;
|
|
49
|
+
fetch: typeof fetch;
|
|
50
|
+
root: string;
|
|
51
|
+
}): Promise<{
|
|
52
|
+
probe: ApiProbe;
|
|
53
|
+
notes: string[];
|
|
54
|
+
}>;
|
|
55
|
+
/** ID pertama dari respons index: array, data[], data.data[], items[], results[]. */
|
|
56
|
+
export declare function firstId(json: unknown): string | number | null;
|
|
57
|
+
/** Route GET (publik, atau semua bila sudah login) → temuan untuk respons ≥500. */
|
|
58
|
+
export declare function smokeApi(probe: ApiProbe): Promise<CheckResult & {
|
|
59
|
+
failed: {
|
|
60
|
+
finding: AuditFinding;
|
|
61
|
+
uri: string;
|
|
62
|
+
}[];
|
|
63
|
+
}>;
|
|
64
|
+
/** Aturan validasi dari isi rules() / $request->validate([...]): field → token aturan. Field bersarang dilewati. */
|
|
65
|
+
export declare function parseRules(source: string): Map<string, string[]>;
|
|
66
|
+
/** Satu nilai yang melanggar aturan per field; field tanpa aturan yang bisa dilanggar tidak diisi. */
|
|
67
|
+
export declare function invalidPayload(rules: Map<string, string[]>): Record<string, unknown>;
|
|
68
|
+
export type ValidationVerdict = 'server_error' | 'accepted' | 'unvalidated' | null;
|
|
69
|
+
/**
|
|
70
|
+
* Hasil satu request uji validasi:
|
|
71
|
+
* - server_error: ≥500 untuk input yang salah;
|
|
72
|
+
* - accepted: 2xx untuk nilai yang melanggar aturan, atau body kosong padahal ada field required;
|
|
73
|
+
* - unvalidated: 2xx untuk body kosong di action yang membaca input tanpa aturan validasi sama sekali.
|
|
74
|
+
*/
|
|
75
|
+
export declare function classifyValidation(status: number, kind: 'empty' | 'invalid', info: {
|
|
76
|
+
hasRules: boolean;
|
|
77
|
+
hasRequired: boolean;
|
|
78
|
+
readsInput: boolean;
|
|
79
|
+
}): ValidationVerdict;
|
|
80
|
+
interface ActionRules {
|
|
81
|
+
rules: Map<string, string[]>;
|
|
82
|
+
readsInput: boolean;
|
|
83
|
+
/** Berkas aturan: FormRequest (baris rules()) atau method controller. */
|
|
84
|
+
location: SourceLocation | null;
|
|
85
|
+
controller: SourceLocation | null;
|
|
86
|
+
}
|
|
87
|
+
/** Aturan validasi action: FormRequest di parameter method, atau validate() / Validator::make di badan method. */
|
|
88
|
+
export declare function actionRules(root: string, action: string): Promise<ActionRules>;
|
|
89
|
+
/** POST/PUT/PATCH: body kosong dan body yang melanggar aturan; satu temuan per route (yang terparah). */
|
|
90
|
+
export declare function checkValidation(probe: ApiProbe): Promise<CheckResult>;
|
|
91
|
+
/**
|
|
92
|
+
* - Route API POST/PUT/PATCH/DELETE tanpa middleware auth (statis dari route list).
|
|
93
|
+
* - Route GET publik di resource yang route lainnya terlindungi, dan mengembalikan data tanpa login.
|
|
94
|
+
* - Route ber-middleware role dipanggil dengan akun berlabel role lain: harus 401/403.
|
|
95
|
+
*/
|
|
96
|
+
export declare function checkAuthorization(probe: ApiProbe, beRoot: string): Promise<CheckResult>;
|
|
97
|
+
export interface ApiCall {
|
|
98
|
+
method: string;
|
|
99
|
+
path: string;
|
|
100
|
+
file: string;
|
|
101
|
+
line: number;
|
|
102
|
+
/** Form dengan _method (method spoofing Laravel): method tidak dibandingkan. */
|
|
103
|
+
spoofed: boolean;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* URL pemanggilan di kode FE → path: base di depan (`${API_URL}/produk`) dibuang, `/${id}` → `/__p__`, query dibuang.
|
|
107
|
+
* null = URL absolut ke host lain, berkas statis, atau bukan path.
|
|
108
|
+
*/
|
|
109
|
+
export declare function normalizeCallPath(raw: string): string | null;
|
|
110
|
+
/** axios-like `.get('/x')` / `.post(\`/x/${id}\`)` dan fetch('/x', { method }) di satu berkas sumber. */
|
|
111
|
+
export declare function extractApiCalls(source: string, file: string): ApiCall[];
|
|
112
|
+
/** Path FE cocok dengan URI route: {param} BE cocok segmen apa pun, __p__ FE cocok segmen apa pun. */
|
|
113
|
+
export declare function routeMatches(uri: string, path: string): boolean;
|
|
114
|
+
/** Path dasar API dari .env.example FE (VITE_*=http…/api) dan axios.create({ baseURL: '/api' }). */
|
|
115
|
+
export declare function basePaths(envExample: string, sources: string[]): string[];
|
|
116
|
+
/** Pemanggilan API di src/ FE yang path-nya tidak ada di BE, atau method-nya tidak diterima route BE. */
|
|
117
|
+
export declare function checkContract(feRoot: string, beRoot: string, routes: Route[]): Promise<CheckResult>;
|
|
118
|
+
export {};
|