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/dist/fix.js CHANGED
@@ -5,27 +5,28 @@
5
5
  // fix-context / autofix-context: ambil konteks + token clone read-only, clone repo di commit sumber, cabut token,
6
6
  // tulis prompt untuk Claude.
7
7
  // claude -p mengubah kode di repo/ (tanpa commit).
8
- // fix-diff: stage perubahan → changes.patch (lockfile & .env dibuang) → artefak.
8
+ // fix-deps: pengganti Claude untuk advisory dependensi (model qaify-deps): composer/npm update lockfile saja,
9
+ // tanpa AI, hasilnya ditulis dalam bentuk keluaran claude -p.
10
+ // fix-diff: stage perubahan → changes.patch (lockfile & .env dibuang, kecuali lockfile mode deps) → artefak.
9
11
  // Job "submit" (VM bersih, kode hasil Claude tidak pernah dijalankan di sini):
10
12
  // fix-submit / autofix-submit: guard patch → token push (contents:write, satu repo) → clone ulang → git apply
11
13
  // → commit → push → cabut token → hasil ke Qaify (backend membuka PR).
12
- import { appendFile, mkdir, writeFile } from 'node:fs/promises';
14
+ import { appendFile, mkdir, readFile, stat, writeFile } from 'node:fs/promises';
13
15
  import { join } from 'node:path';
14
16
  import { defaultExec, gitAuthEnv, revokeTokens } from './audit.js';
15
17
  import { parseClaudeUsage, readClaudeUsage } from './usage.js';
16
18
  /** Model untuk --model Claude Code: alias atau ID versi; selain itu ditolak (nilainya masuk ke shell workflow). */
17
19
  const MODEL_PATTERN = /^[a-z0-9][a-z0-9.-]{0,63}$/;
20
+ /** "Model" perbaikan advisory dependensi tanpa AI; cermin config qafactory.audit_fix.deps_model. */
21
+ export const DEPS_MODEL = 'qaify-deps';
18
22
  /** Berkas yang tidak boleh diubah perbaikan: workflow (bisa membaca secret repo), rahasia, lockfile. */
19
23
  const BLOCKED = [/^\.github\//, /(^|\/)\.env(\.(?!example$)[^/]*)?$/];
24
+ const LOCKFILE = /(^|\/)(package-lock\.json|yarn\.lock|pnpm-lock\.yaml|composer\.lock|bun\.lockb?)$/;
20
25
  /**
21
26
  * Ikut berubah tanpa sengaja saat Claude menjalankan npm / composer install atau Playwright (laporan & trace bila
22
27
  * repo tidak meng-ignore-nya): dibuang dari patch, bukan menggagalkan.
23
28
  */
24
- const INCIDENTAL = [
25
- /(^|\/)(package-lock\.json|yarn\.lock|pnpm-lock\.yaml|composer\.lock|bun\.lockb?)$/,
26
- /(^|\/)\.env(\.(?!example$)[^/]*)?$/,
27
- /^(test-results|playwright-report|blob-report)\//,
28
- ];
29
+ const INCIDENTAL = [LOCKFILE, /(^|\/)\.env(\.(?!example$)[^/]*)?$/, /^(test-results|playwright-report|blob-report)\//];
29
30
  export const FIX_SCHEMA = {
30
31
  type: 'object',
31
32
  properties: {
@@ -137,12 +138,13 @@ export async function fixContext(client, fixId, dir, opts = {}) {
137
138
  const ctx = (await client.get(`${FIX_BASE[kind]}/${encodeURIComponent(fixId)}/context${query}`));
138
139
  if (!ctx?.repo?.token) {
139
140
  log('qa-factory: konteks perbaikan tidak bisa diambil (perbaikan tidak dikenal / sudah selesai / token runner salah)');
140
- return { ok: false, model: null };
141
+ return { ok: false, model: null, mode: 'claude' };
141
142
  }
143
+ const deps = kind === 'audit' && ctx.deps && /^[@\w][\w./@-]*$/.test(ctx.deps.package) && ['composer', 'npm'].includes(ctx.deps.manager) ? ctx.deps : null;
142
144
  if (!MODEL_PATTERN.test(ctx.model)) {
143
145
  log(`qa-factory: model "${ctx.model}" tidak valid`);
144
146
  await revokeTokens([ctx.repo.token], opts.fetch);
145
- return { ok: false, model: null };
147
+ return { ok: false, model: null, mode: 'claude' };
146
148
  }
147
149
  if (process.env.GITHUB_ACTIONS === 'true')
148
150
  console.log(`::add-mask::${ctx.repo.token}`);
@@ -157,26 +159,120 @@ export async function fixContext(client, fixId, dir, opts = {}) {
157
159
  catch (e) {
158
160
  log(`qa-factory: clone gagal: ${e.message.split('\n')[0]}`);
159
161
  await revokeTokens([ctx.repo.token], opts.fetch);
160
- return { ok: false, model: null };
162
+ return { ok: false, model: null, mode: 'claude' };
161
163
  }
162
164
  await revokeTokens([ctx.repo.token], opts.fetch);
163
165
  const { token: _token, ...repo } = ctx.repo;
164
166
  await mkdir(join(dir, '.qaify'), { recursive: true });
167
+ if (deps) {
168
+ await writeFile(join(dir, '.qaify', 'deps.json'), JSON.stringify(deps));
169
+ log(`qa-factory: ${ctx.repo.full_name}@${ref.slice(0, 12)} → repo/ (update ${deps.manager} ${deps.package} tanpa AI, percobaan ke-${ctx.attempt_no})`);
170
+ return { ok: true, model: ctx.model, mode: 'deps' };
171
+ }
165
172
  await writeFile(join(dir, '.qaify', 'prompt.txt'), kind === 'test' ? testFixPrompt({ ...ctx, repo }) : fixPrompt({ ...ctx, repo }));
166
173
  const detail = kind === 'test' ? ctx.failure.category : ctx.complexity;
167
174
  log(`qa-factory: ${ctx.repo.full_name}@${ref.slice(0, 12)} → repo/ (model ${ctx.model}, ${detail}, percobaan ke-${ctx.attempt_no})`);
168
- return { ok: true, model: ctx.model };
175
+ return { ok: true, model: ctx.model, mode: 'claude' };
176
+ }
177
+ /**
178
+ * Advisory dependensi tanpa AI: update paket di lockfile saja (tanpa install / script), cek ulang dengan audit, lalu
179
+ * tulis hasilnya ke <dir>/.qaify/claude.json dalam bentuk keluaran `claude -p` (model qaify-deps, token 0) agar
180
+ * fix-diff / fix-submit tidak berubah. npm: npm update dulu, npm audit fix bila paket masih rentan.
181
+ */
182
+ export async function fixDeps(dir, exec = defaultExec) {
183
+ const started = Date.now();
184
+ const target = JSON.parse(await readFile(join(dir, '.qaify', 'deps.json'), 'utf8'));
185
+ const repo = join(dir, 'repo');
186
+ const pkg = target.package;
187
+ // composer/npm audit keluar non-0 bila masih ada kerentanan: laporannya tetap di stdout
188
+ const report = (file, args) => exec(file, args).catch((e) => (typeof e?.stdout === 'string' ? e.stdout : ''));
189
+ const composer = target.manager === 'composer';
190
+ const manifest = composer ? 'composer.json' : 'package.json';
191
+ const auditCmd = composer ? 'composer audit --locked' : 'npm audit --package-lock-only';
192
+ const steps = [];
193
+ let vulnerable;
194
+ try {
195
+ if (composer) {
196
+ steps.push(`composer update ${pkg} --with-all-dependencies --no-install`);
197
+ await exec('composer', ['update', pkg, '--with-all-dependencies', '--no-install', '--no-scripts', '--no-interaction', '--no-progress', `--working-dir=${repo}`]);
198
+ vulnerable = stillVulnerable(await report('composer', ['audit', '--locked', '--format=json', '--no-interaction', `--working-dir=${repo}`]), 'composer', pkg);
199
+ }
200
+ else {
201
+ steps.push(`npm update ${pkg} --package-lock-only`);
202
+ await exec('npm', ['update', pkg, '--package-lock-only', '--no-fund', '--no-audit', '--prefix', repo]);
203
+ const audit = () => report('npm', ['audit', '--json', '--package-lock-only', '--prefix', repo]);
204
+ vulnerable = stillVulnerable(await audit(), 'npm', pkg);
205
+ if (vulnerable) {
206
+ steps.push('npm audit fix --package-lock-only');
207
+ await exec('npm', ['audit', 'fix', '--package-lock-only', '--no-fund', '--prefix', repo]).catch(() => '');
208
+ vulnerable = stillVulnerable(await audit(), 'npm', pkg);
209
+ }
210
+ }
211
+ }
212
+ catch (e) {
213
+ const message = String(e.stderr || e.message).trim().split('\n').slice(-3).join(' ');
214
+ return writeDeps(dir, started, { fixed: false, summary: `Gagal memperbarui ${pkg}.`, reason: `${steps.at(-1)} gagal: ${message}`.slice(0, 1000) });
215
+ }
216
+ const changed = (await exec('git', ['-C', repo, 'status', '--porcelain'])).split('\n').map((l) => l.slice(3).trim()).filter(Boolean);
217
+ if (vulnerable) {
218
+ return writeDeps(dir, started, {
219
+ fixed: false,
220
+ summary: `${pkg} masih rentan setelah ${steps.join(' lalu ')}.`,
221
+ reason: `Versi ${pkg} yang sudah diperbaiki berada di luar constraint ${manifest} (kemungkinan perlu naik versi major). Naikkan constraint ${pkg} di ${manifest} secara manual lalu uji aplikasinya.`,
222
+ });
223
+ }
224
+ if (!changed.length) {
225
+ return writeDeps(dir, started, { fixed: false, summary: `Lockfile tidak berubah.`, reason: `Advisory ${pkg} sudah tidak muncul di ${auditCmd} commit ini; temuan mungkin sudah diperbaiki.` });
226
+ }
227
+ return writeDeps(dir, started, {
228
+ fixed: true,
229
+ summary: `- ${pkg} diperbarui dengan ${steps.join(', lalu ')} (tanpa AI).\n- ${auditCmd} tidak lagi melaporkan ${pkg}.`,
230
+ files_changed: changed,
231
+ tests_run: `${auditCmd}: ${pkg} bersih. Test aplikasi dijalankan CI repo pada PR ini.`,
232
+ });
233
+ }
234
+ /** Masih ada advisory untuk paket ini? npm: hanya high / critical (sama dengan temuan audit-static). */
235
+ export function stillVulnerable(output, manager, pkg) {
236
+ const start = output.search(/[[{]/);
237
+ let data = null;
238
+ try {
239
+ data = start < 0 ? null : JSON.parse(output.slice(start));
240
+ }
241
+ catch {
242
+ // laporan tidak terbaca: dianggap masih rentan, jangan buka PR yang belum terbukti
243
+ }
244
+ if (!data)
245
+ return true;
246
+ if (manager === 'composer') {
247
+ const list = data.advisories?.[pkg];
248
+ return Array.isArray(list) ? list.length > 0 : !!list && Object.keys(list).length > 0;
249
+ }
250
+ const vuln = data.vulnerabilities?.[pkg];
251
+ return vuln?.severity === 'high' || vuln?.severity === 'critical';
252
+ }
253
+ async function writeDeps(dir, started, result) {
254
+ await writeFile(join(dir, '.qaify', 'claude.json'), JSON.stringify({
255
+ type: 'result',
256
+ subtype: 'success',
257
+ duration_ms: Date.now() - started,
258
+ structured_output: result,
259
+ usage: { input_tokens: 0, output_tokens: 0 },
260
+ modelUsage: { [DEPS_MODEL]: { outputTokens: 0 } },
261
+ }));
262
+ return result;
169
263
  }
170
264
  /**
171
265
  * Perubahan Claude di <dir>/repo → <out>/changes.patch (git diff --binary). Lockfile & .env yang ikut berubah saat
172
- * menjalankan aplikasi dibuang dari patch. Job ini tidak memegang token, jadi aman menjalankan git di repo hasil Claude.
266
+ * menjalankan aplikasi dibuang dari patch, kecuali lockfile pada mode deps (.qaify/deps.json) yang memang hasilnya.
267
+ * Job ini tidak memegang token, jadi aman menjalankan git di repo hasil Claude.
173
268
  */
174
269
  export async function fixDiff(dir, out, exec = defaultExec) {
175
270
  const repo = join(dir, 'repo');
176
271
  await mkdir(out, { recursive: true });
177
272
  await exec('git', ['-C', repo, 'add', '-A']);
273
+ const deps = await stat(join(dir, '.qaify', 'deps.json')).then(() => true, () => false);
178
274
  const staged = (await exec('git', ['-C', repo, 'diff', '--cached', '--name-only'])).split('\n').filter(Boolean);
179
- const dropped = staged.filter((f) => INCIDENTAL.some((re) => re.test(f)));
275
+ const dropped = staged.filter((f) => INCIDENTAL.some((re) => re.test(f)) && !(deps && LOCKFILE.test(f)));
180
276
  if (dropped.length)
181
277
  await exec('git', ['-C', repo, 'reset', '-q', '--', ...dropped]);
182
278
  await writeFile(join(out, 'changes.patch'), await exec('git', ['-C', repo, 'diff', '--cached', '--binary']));
@@ -189,6 +285,7 @@ export function patchStats(patch) {
189
285
  const paths = new Set();
190
286
  let unreadable = 0;
191
287
  let changedLines = 0;
288
+ let lockfile = false;
192
289
  for (const line of patch.split('\n')) {
193
290
  if (line.startsWith('diff --git ')) {
194
291
  const header = /^diff --git "?a\/(.+?)"? "?b\/(.+?)"?$/.exec(line);
@@ -199,8 +296,9 @@ export function patchStats(patch) {
199
296
  paths.add(header[1]);
200
297
  paths.add(header[2]);
201
298
  files.add(header[2]);
299
+ lockfile = LOCKFILE.test(header[2]);
202
300
  }
203
- else if (/^(\+(?!\+\+ )|-(?!-- ))/.test(line)) {
301
+ else if (!lockfile && /^(\+(?!\+\+ )|-(?!-- ))/.test(line)) {
204
302
  changedLines++;
205
303
  }
206
304
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "qaify-playwright-reporter",
3
- "version": "0.7.0",
3
+ "version": "0.9.0",
4
4
  "description": "Reporter Playwright untuk QA Factory (Qaify): kirim hasil test dan artefak ke /api/ingest, tanpa pernah menggagalkan test.",
5
5
  "license": "MIT",
6
6
  "author": "ahmdkenz",
@@ -3,13 +3,14 @@
3
3
  # Butuh di repo runner ini (Settings → Secrets and variables → Actions):
4
4
  # secret QA_FACTORY_TOKEN token reporter repo runner (Pengaturan → Token Reporter)
5
5
  # variable QA_FACTORY_URL URL API Qaify
6
- # secret CLAUDE_CODE_OAUTH_TOKEN opsional: audit dengan Claude (langganan Claude Pro/Max Anda, `claude setup-token`)
7
- # variable QA_FACTORY_AUDIT_MODE opsional, `static` atau `claude` (default: claude bila secret di atas ada)
6
+ # secret CLAUDE_CODE_OAUTH_TOKEN opsional: perbaikan temuan & audit mode claude (langganan Claude Pro/Max, `claude setup-token`)
7
+ # variable QA_FACTORY_AUDIT_MODE opsional, `static` / `claude`; hanya dipakai bila Qaify tidak mengirim mode
8
8
  # variable QA_FACTORY_AI_MODEL opsional, default sonnet
9
9
  # variable QA_FACTORY_MCP_VERSION opsional, versi @playwright/mcp (default latest)
10
10
  # GitHub App Qaify harus terpasang di repo ini dan di repo FE + BE. Nama file harus tetap qaify-audit.yml.
11
+ # Mode audit dipilih di Qaify (setelan aplikasi, bisa diganti per audit); mode claude tanpa secret jatuh ke static.
11
12
  # Mode static (tanpa AI): build, test, composer/npm audit, dan smoke test Playwright; cepat, tanpa kuota AI,
12
- # tetapi tanpa analisis logika bisnis / kontrak API dari kode.
13
+ # tetapi tanpa analisis logika bisnis / kontrak API dari kode. Temuannya tetap bisa diperbaiki Claude (qaify-fix.yml).
13
14
  # Mode claude: satu sesi Claude yang panjang; kuota langganan dipakai bersama pemakaian pribadi.
14
15
  name: Qaify audit
15
16
  run-name: audit ${{ inputs.audit_id }}
@@ -42,9 +43,8 @@ jobs:
42
43
  env: { POSTGRES_PASSWORD: postgres, POSTGRES_DB: app }
43
44
  ports: ['5432:5432']
44
45
  options: --health-cmd="pg_isready -U postgres" --health-interval=10s --health-timeout=5s --health-retries=10
45
- # context runner tidak tersedia di env level job; WORK diisi lewat GITHUB_ENV di step pertama
46
- env:
47
- AUDIT_MODE: ${{ vars.QA_FACTORY_AUDIT_MODE || (secrets.CLAUDE_CODE_OAUTH_TOKEN != '' && 'claude' || 'static') }}
46
+ # context runner tidak tersedia di env level job; WORK diisi lewat GITHUB_ENV di step pertama,
47
+ # AUDIT_MODE di step "Tentukan mode audit" (mode baru diketahui dari konteks audit)
48
48
  steps:
49
49
  - name: Siapkan direktori kerja
50
50
  run: echo "WORK=$RUNNER_TEMP/qaify" >> "$GITHUB_ENV"
@@ -58,9 +58,7 @@ jobs:
58
58
  tools: composer
59
59
 
60
60
  - name: Pasang Qaify CLI
61
- run: |
62
- npm install -g qaify-playwright-reporter@0.5 playwright-core@1
63
- if [ "$AUDIT_MODE" = claude ]; then npm install -g @anthropic-ai/claude-code; fi
61
+ run: npm install -g qaify-playwright-reporter@0.9 playwright-core@1
64
62
 
65
63
  # token clone sekali pakai (read-only, hanya repo FE + BE) dicabut sebelum auditor berjalan
66
64
  - name: Ambil konteks & clone repo FE + BE
@@ -70,6 +68,25 @@ jobs:
70
68
  QA_FACTORY_TOKEN: ${{ secrets.QA_FACTORY_TOKEN }}
71
69
  run: qaify audit-context --audit "${{ inputs.audit_id }}" --dir "$WORK"
72
70
 
71
+ # pilihan Qaify → variable QA_FACTORY_AUDIT_MODE → claude bila secret ada; claude tanpa secret → static
72
+ - name: Tentukan mode audit
73
+ if: steps.ctx.outputs.ok == 'true'
74
+ env:
75
+ REQUESTED: ${{ steps.ctx.outputs.mode || vars.QA_FACTORY_AUDIT_MODE }}
76
+ HAS_CLAUDE: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN != '' }}
77
+ run: |
78
+ mode="${REQUESTED:-$([ "$HAS_CLAUDE" = true ] && echo claude || echo static)}"
79
+ if [ "$mode" != static ] && [ "$mode" != claude ]; then
80
+ echo "::warning::Mode audit tidak dikenal ($mode); memakai static."
81
+ mode=static
82
+ fi
83
+ if [ "$mode" = claude ] && [ "$HAS_CLAUDE" != true ]; then
84
+ echo "::warning::Mode claude diminta tetapi secret CLAUDE_CODE_OAUTH_TOKEN kosong; audit dijalankan tanpa AI."
85
+ mode=static
86
+ fi
87
+ echo "qa-factory: mode audit $mode"
88
+ echo "AUDIT_MODE=$mode" >> "$GITHUB_ENV"
89
+
73
90
  # tanpa secret apa pun: kode aplikasi (composer/npm script) berjalan di step ini
74
91
  - name: Audit tanpa AI (Playwright)
75
92
  if: steps.ctx.outputs.ok == 'true' && env.AUDIT_MODE == 'static'
@@ -80,6 +97,7 @@ jobs:
80
97
  - name: Siapkan instruksi & Playwright MCP
81
98
  if: steps.ctx.outputs.ok == 'true' && env.AUDIT_MODE == 'claude'
82
99
  run: |
100
+ npm install -g @anthropic-ai/claude-code
83
101
  mkdir -p "$WORK/.qaify"
84
102
  qaify audit-schema --out "$WORK/.qaify/schema.json" --instructions "$WORK/.qaify/instructions.txt"
85
103
  # Google Chrome sudah ada di runner ubuntu-latest
@@ -6,6 +6,8 @@
6
6
  # secret CLAUDE_CODE_OAUTH_TOKEN langganan Claude Pro/Max Anda (`claude setup-token`); wajib untuk perbaikan
7
7
  # GitHub App Qaify butuh izin Contents: Read and write dan Pull requests: Read and write di repo FE/BE.
8
8
  # Model (haiku / sonnet / opus atau versi tertentu) dipilih Qaify per temuan menurut kompleksitas atau pilihan Anda.
9
+ # Advisory dependensi (composer.lock / package-lock.json) diperbaiki tanpa Claude: `qaify fix-deps` memperbarui
10
+ # paketnya di lockfile (mode deps), tanpa memakai kuota.
9
11
  # Job "fix" menjalankan Claude tanpa token push; job "submit" (VM bersih) meng-clone ulang, menerapkan patch, lalu push.
10
12
  # Nama file harus tetap qaify-fix.yml.
11
13
  name: Qaify fix
@@ -54,7 +56,7 @@ jobs:
54
56
  tools: composer
55
57
 
56
58
  - name: Pasang Qaify CLI & Claude Code
57
- run: npm install -g qaify-playwright-reporter@0.5 @anthropic-ai/claude-code
59
+ run: npm install -g qaify-playwright-reporter@0.9 @anthropic-ai/claude-code
58
60
 
59
61
  # token clone sekali pakai (read-only, hanya repo target) dicabut sebelum Claude berjalan
60
62
  - name: Ambil temuan & clone repo target
@@ -66,9 +68,16 @@ jobs:
66
68
  qaify fix-context --fix "${{ inputs.fix_id }}" --dir "$WORK"
67
69
  qaify fix-schema --out "$WORK/.qaify/schema.json" --instructions "$WORK/.qaify/instructions.txt"
68
70
 
71
+ # advisory dependensi: update lockfile tanpa AI, tidak ada token apa pun di step ini
72
+ - name: Perbarui dependensi (tanpa AI)
73
+ if: steps.ctx.outputs.ok == 'true' && steps.ctx.outputs.mode == 'deps'
74
+ continue-on-error: true
75
+ timeout-minutes: 15
76
+ run: qaify fix-deps --dir "$WORK"
77
+
69
78
  # hanya CLAUDE_CODE_OAUTH_TOKEN yang tersedia di step ini; tidak ada token Qaify maupun token push
70
79
  - name: Perbaiki dengan Claude Code (Claude Pro/Max)
71
- if: steps.ctx.outputs.ok == 'true'
80
+ if: steps.ctx.outputs.ok == 'true' && steps.ctx.outputs.mode != 'deps'
72
81
  continue-on-error: true
73
82
  timeout-minutes: 45
74
83
  working-directory: ${{ runner.temp }}/qaify/repo
@@ -109,7 +118,7 @@ jobs:
109
118
  - uses: actions/setup-node@v4
110
119
  with: { node-version: 22 }
111
120
  - name: Pasang Qaify CLI
112
- run: npm install -g qaify-playwright-reporter@0.5
121
+ run: npm install -g qaify-playwright-reporter@0.9
113
122
  - uses: actions/download-artifact@v4
114
123
  continue-on-error: true
115
124
  with: