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 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
- - Tanpa secret itu, audit berjalan **tanpa AI** (`qaify audit-static`): build, test, `composer audit` / `npm audit`, route GET publik 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.
111
- - Dengan secret itu, Claude yang mengaudit. Hasilnya lebih dalam, termasuk logika bisnis dan kontrak API.
112
- - Mode bisa dipaksa lewat variable `QA_FACTORY_AUDIT_MODE` (`static` / `claude`).
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
- qaify fix-diff --dir "$WORK" --out "$WORK/out" # changes.patch (lockfile & .env dibuang) → artefak
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 {};