qaify-playwright-reporter 0.8.0 → 0.9.1

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
@@ -109,11 +109,18 @@ Untuk aplikasi yang repo FE dan BE-nya terpisah dan belum memasang Playwright, Q
109
109
 
110
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
111
 
112
- - **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.
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).
113
120
  - **Claude**: Claude yang mengaudit. Hasilnya lebih dalam, termasuk logika bisnis dan kontrak API, tetapi memakan kuota langganan.
114
121
  - Variable `QA_FACTORY_AUDIT_MODE` (`static` / `claude`) hanya dipakai bila backend Qaify belum mengirim mode (versi lama).
115
122
 
116
- 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.
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.
117
124
  4. Di Qaify → **Aplikasi**, tambahkan aplikasi (repo FE, BE, runner), lalu klik **Audit sekarang**.
118
125
 
119
126
  Alur di workflow:
@@ -152,6 +159,8 @@ Model dipilih Qaify per temuan dan dikirim lewat konteks. Kamu tidak perlu mengi
152
159
 
153
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.
154
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. Claude tetap bisa dipilih manual per temuan dependensi: untuk temuan itu Claude boleh menaikkan constraint dan mengubah lockfile paketnya, dan lockfile ikut di patch.
163
+
155
164
  Alur di workflow:
156
165
 
157
166
  ```bash
@@ -159,15 +168,17 @@ Alur di workflow:
159
168
  qaify fix-context --fix <id> --dir "$WORK" # clone repo target di commit yang diaudit (token read-only, lalu dicabut)
160
169
  qaify fix-schema --out schema.json --instructions instructions.txt
161
170
  claude -p "$(cat .qaify/prompt.txt)" --model "$MODEL" … # ubah kode + jalankan test, tanpa commit
162
- 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
163
174
  # job submit: VM bersih
164
175
  qaify fix-submit --fix <id> --results claude.json --patch changes.patch --dir "$RUNNER_TEMP/push"
165
176
  ```
166
177
 
167
178
  Keamanan:
168
179
  - Job Claude tidak pernah memegang token push maupun `QA_FACTORY_TOKEN`.
169
- - 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.
170
- - Patch ditolak bila menyentuh `.github/` atau `.env`, atau melebihi 20 berkas / 600 baris.
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. Branch perbaikan dibuat dari ujung base branch (commit yang diaudit hanya cadangan bila patch tidak cocok), karena GitHub menolak token App yang membuat branch dari commit dengan `.github/workflows/` berbeda dari branch utama. Bila push / apply gagal, pesan git dari GitHub dikirim utuh ke Qaify beserta petunjuknya. Token push (`contents:write`) hanya berlaku untuk satu repo, sekali pakai, dan dicabut setelah push.
181
+ - Patch ditolak bila menyentuh `.github/` atau `.env`, atau melebihi 20 berkas / 600 baris (baris lockfile tidak dihitung).
171
182
 
172
183
  ## Uji lokal tanpa GitHub Actions
173
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 {};