qaify-playwright-reporter 0.5.0 → 0.6.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/ai.d.ts +2 -5
- package/dist/ai.js +3 -14
- package/dist/audit.d.ts +9 -9
- package/dist/audit.js +28 -20
- package/dist/cli.js +5 -2
- package/dist/fix.d.ts +2 -7
- package/dist/fix.js +5 -12
- package/dist/usage.d.ts +18 -0
- package/dist/usage.js +60 -0
- package/package.json +1 -1
- package/templates/qaify-audit.yml +2 -2
package/dist/ai.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type ClaudeUsage } from './usage.js';
|
|
1
2
|
export declare const CATEGORIES: readonly ["app_bug", "outdated_test", "flaky", "infra", "unknown"];
|
|
2
3
|
/** --json-schema untuk claude -p: hasilnya ada di field structured_output. */
|
|
3
4
|
export declare const RESULT_SCHEMA: {
|
|
@@ -40,11 +41,7 @@ export interface AiResult {
|
|
|
40
41
|
summary: string;
|
|
41
42
|
confidence: number;
|
|
42
43
|
}
|
|
43
|
-
export
|
|
44
|
-
model: string | null;
|
|
45
|
-
input_tokens: number;
|
|
46
|
-
output_tokens: number;
|
|
47
|
-
}
|
|
44
|
+
export type AiUsage = ClaudeUsage;
|
|
48
45
|
/**
|
|
49
46
|
* Baca output `claude -p --output-format json --json-schema …`.
|
|
50
47
|
* Output kosong / rusak / tanpa structured_output (gagal login, kuota habis, ditolak) → results kosong.
|
package/dist/ai.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// Mode actions: kegagalan dianalisis Claude Code (claude -p) di job analyze dengan langganan Claude Pro/Max.
|
|
2
2
|
// Backend tidak memanggil AI; reporter hanya mengambil antrean dan mengirim balik hasilnya.
|
|
3
|
+
import { parseClaudeUsage, readClaudeUsage } from './usage.js';
|
|
3
4
|
export const CATEGORIES = ['app_bug', 'outdated_test', 'flaky', 'infra', 'unknown'];
|
|
4
5
|
/** --json-schema untuk claude -p: hasilnya ada di field structured_output. */
|
|
5
6
|
export const RESULT_SCHEMA = {
|
|
@@ -43,21 +44,9 @@ export function parseClaudeOutput(raw) {
|
|
|
43
44
|
parsed = JSON.parse(raw);
|
|
44
45
|
}
|
|
45
46
|
catch {
|
|
46
|
-
return { results: [], usage:
|
|
47
|
+
return { results: [], usage: readClaudeUsage(raw) };
|
|
47
48
|
}
|
|
48
49
|
const items = Array.isArray(parsed?.structured_output?.results) ? parsed.structured_output.results : [];
|
|
49
50
|
const results = items.filter((r) => Number.isInteger(r?.failure_id) && typeof r?.category === 'string' && typeof r?.summary === 'string' && typeof r?.confidence === 'number');
|
|
50
|
-
|
|
51
|
-
const models = Object.entries((parsed?.modelUsage ?? {}));
|
|
52
|
-
const model = models.sort(([, a], [, b]) => (b.outputTokens ?? 0) - (a.outputTokens ?? 0))[0]?.[0] ?? null;
|
|
53
|
-
// token input Claude Code sebagian besar tercatat sebagai cache (system prompt); semuanya dijumlah
|
|
54
|
-
const u = parsed?.usage;
|
|
55
|
-
const usage = u
|
|
56
|
-
? {
|
|
57
|
-
model,
|
|
58
|
-
input_tokens: Number(u.input_tokens ?? 0) + Number(u.cache_creation_input_tokens ?? 0) + Number(u.cache_read_input_tokens ?? 0),
|
|
59
|
-
output_tokens: Number(u.output_tokens ?? 0),
|
|
60
|
-
}
|
|
61
|
-
: null;
|
|
62
|
-
return { results, usage };
|
|
51
|
+
return { results, usage: parseClaudeUsage(parsed) };
|
|
63
52
|
}
|
package/dist/audit.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type IngestClient } from './client.js';
|
|
2
|
+
import { type ClaudeUsage } from './usage.js';
|
|
2
3
|
export declare const AREAS: readonly ["ui", "business_logic", "api_contract", "validation", "error_handling", "security", "performance"];
|
|
3
4
|
export declare const SEVERITIES: readonly ["critical", "high", "medium", "low"];
|
|
4
5
|
export declare const ROLES: readonly ["fe", "be"];
|
|
@@ -262,18 +263,11 @@ export interface AuditPage {
|
|
|
262
263
|
file?: string;
|
|
263
264
|
line?: number;
|
|
264
265
|
}
|
|
265
|
-
export interface AuditUsage {
|
|
266
|
-
model: string | null;
|
|
267
|
-
input_tokens: number;
|
|
268
|
-
output_tokens: number;
|
|
269
|
-
num_turns: number | null;
|
|
270
|
-
duration_ms: number | null;
|
|
271
|
-
}
|
|
272
266
|
export interface ParsedAudit {
|
|
273
267
|
summary: Record<string, unknown>;
|
|
274
268
|
findings: AuditFinding[];
|
|
275
269
|
pages: AuditPage[];
|
|
276
|
-
usage:
|
|
270
|
+
usage: ClaudeUsage | null;
|
|
277
271
|
}
|
|
278
272
|
/**
|
|
279
273
|
* Baca output `claude -p --output-format json --json-schema AUDIT_SCHEMA`.
|
|
@@ -285,6 +279,8 @@ export declare const defaultExec: Exec;
|
|
|
285
279
|
export interface AuditContextResult {
|
|
286
280
|
ok: boolean;
|
|
287
281
|
commits: Record<string, string>;
|
|
282
|
+
/** alasan gagal; juga ditulis ke context-error.txt agar audit-submit bisa melaporkannya */
|
|
283
|
+
error?: string;
|
|
288
284
|
}
|
|
289
285
|
/**
|
|
290
286
|
* GET /api/ingest/audits/{id}/context → clone apps/<role> → cabut token.
|
|
@@ -297,6 +293,8 @@ export declare function auditContext(client: IngestClient, auditId: string, dir:
|
|
|
297
293
|
fetch?: typeof fetch;
|
|
298
294
|
log?: (msg: string) => void;
|
|
299
295
|
}): Promise<AuditContextResult>;
|
|
296
|
+
/** Alasan gagal dari audit-context (context-error.txt di samping context.json), atau null. */
|
|
297
|
+
export declare function readContextError(contextFile: string): Promise<string | null>;
|
|
300
298
|
/** Token GitHub untuk git lewat header dari environment: tidak pernah di argumen, URL remote, atau .git/config. */
|
|
301
299
|
export declare function gitAuthEnv(token: string): NodeJS.ProcessEnv;
|
|
302
300
|
/** DELETE /installation/token: token tidak bisa dipakai lagi oleh langkah berikutnya. */
|
|
@@ -305,7 +303,9 @@ export declare function revokeTokens(tokens: string[], doFetch?: typeof fetch):
|
|
|
305
303
|
* Kirim hasil audit. Tanpa hasil terstruktur → audit ditandai gagal dengan penyebab yang mungkin.
|
|
306
304
|
* Screenshot (temuan + galeri halaman) hanya diambil dari dalam folder evidence (path di luar folder itu diabaikan).
|
|
307
305
|
*/
|
|
308
|
-
export declare function auditSubmit(client: IngestClient, auditId: string, claudeOutput: string, evidenceDir: string, commits: Record<string, string>, log?: (msg: string) => void
|
|
306
|
+
export declare function auditSubmit(client: IngestClient, auditId: string, claudeOutput: string, evidenceDir: string, commits: Record<string, string>, log?: (msg: string) => void,
|
|
307
|
+
/** dari audit-context: auditor tidak dijalankan sama sekali */
|
|
308
|
+
contextError?: string | null): Promise<any>;
|
|
309
309
|
/** Penyebab screenshot tidak terunggah, agar laporan "Tanpa screenshot" tidak terjadi diam-diam. */
|
|
310
310
|
export declare function uploadProblems(res: any, unsigned: number, rejected: number, putError: string | null): string[];
|
|
311
311
|
/** Commit per role dari context.json (dibuat audit-context). */
|
package/dist/audit.js
CHANGED
|
@@ -4,9 +4,10 @@
|
|
|
4
4
|
// audit-submit: unggah screenshot bukti (presigned) lalu kirim hasil ke Qaify.
|
|
5
5
|
import { execFile } from 'node:child_process';
|
|
6
6
|
import { mkdir, readFile, stat, writeFile } from 'node:fs/promises';
|
|
7
|
-
import { extname, join, resolve, sep } from 'node:path';
|
|
7
|
+
import { dirname, extname, join, resolve, sep } from 'node:path';
|
|
8
8
|
import { promisify } from 'node:util';
|
|
9
9
|
import { warn } from './client.js';
|
|
10
|
+
import { parseClaudeUsage, readClaudeUsage } from './usage.js';
|
|
10
11
|
export const AREAS = ['ui', 'business_logic', 'api_contract', 'validation', 'error_handling', 'security', 'performance'];
|
|
11
12
|
export const SEVERITIES = ['critical', 'high', 'medium', 'low'];
|
|
12
13
|
export const ROLES = ['fe', 'be'];
|
|
@@ -168,23 +169,12 @@ export function parseAuditOutput(raw) {
|
|
|
168
169
|
return null;
|
|
169
170
|
const findings = out.findings.filter((f) => ROLES.includes(f?.repo) && typeof f?.title === 'string' && typeof f?.description === 'string');
|
|
170
171
|
const pages = (Array.isArray(out.pages) ? out.pages : []).filter((p) => typeof p?.path === 'string' && VIEWPORTS.includes(p?.viewport));
|
|
171
|
-
|
|
172
|
-
const model = models.sort(([, a], [, b]) => (b.outputTokens ?? 0) - (a.outputTokens ?? 0))[0]?.[0] ?? null;
|
|
173
|
-
const u = parsed?.usage;
|
|
174
|
-
const usage = u
|
|
175
|
-
? {
|
|
176
|
-
model,
|
|
177
|
-
input_tokens: Number(u.input_tokens ?? 0) + Number(u.cache_creation_input_tokens ?? 0) + Number(u.cache_read_input_tokens ?? 0),
|
|
178
|
-
output_tokens: Number(u.output_tokens ?? 0),
|
|
179
|
-
num_turns: Number.isInteger(parsed?.num_turns) ? parsed.num_turns : null,
|
|
180
|
-
duration_ms: Number.isInteger(parsed?.duration_ms) ? parsed.duration_ms : null,
|
|
181
|
-
}
|
|
182
|
-
: null;
|
|
183
|
-
return { summary: out.summary && typeof out.summary === 'object' ? out.summary : {}, findings, pages, usage };
|
|
172
|
+
return { summary: out.summary && typeof out.summary === 'object' ? out.summary : {}, findings, pages, usage: parseClaudeUsage(parsed) };
|
|
184
173
|
}
|
|
185
174
|
const execFileAsync = promisify(execFile);
|
|
186
175
|
export const defaultExec = async (file, args, env) => (await execFileAsync(file, args, { env: { ...process.env, ...env }, maxBuffer: 10 * 1024 * 1024 })).stdout;
|
|
187
176
|
const branchOf = (repo) => repo.branch || repo.default_branch;
|
|
177
|
+
const CONTEXT_ERROR_FILE = 'context-error.txt';
|
|
188
178
|
/**
|
|
189
179
|
* GET /api/ingest/audits/{id}/context → clone apps/<role> → cabut token.
|
|
190
180
|
* Token hanya lewat header git dari environment (tidak pernah di argumen, URL remote, atau disk)
|
|
@@ -196,8 +186,7 @@ export async function auditContext(client, auditId, dir, opts = {}) {
|
|
|
196
186
|
const query = opts.githubRunId ? `?github_run_id=${opts.githubRunId}` : '';
|
|
197
187
|
const ctx = await client.get(`/api/ingest/audits/${encodeURIComponent(auditId)}/context${query}`);
|
|
198
188
|
if (!ctx || !Array.isArray(ctx.repos)) {
|
|
199
|
-
|
|
200
|
-
return { ok: false, commits: {} };
|
|
189
|
+
return contextFailed(dir, 'Konteks audit tidak bisa diambil (audit tidak dikenal / sudah selesai / token runner salah).', {});
|
|
201
190
|
}
|
|
202
191
|
const repos = ctx.repos;
|
|
203
192
|
// GitHub Actions menyamarkan token di log
|
|
@@ -219,9 +208,10 @@ export async function auditContext(client, auditId, dir, opts = {}) {
|
|
|
219
208
|
}
|
|
220
209
|
}
|
|
221
210
|
catch (e) {
|
|
222
|
-
log(`qa-factory: clone gagal: ${e.message.split('\n')[0]}`);
|
|
223
211
|
await revoke(repos, opts.fetch);
|
|
224
|
-
|
|
212
|
+
// baris terakhir stderr git berisi penyebabnya (mis. "Remote branch x not found"); token tidak pernah ada di pesan
|
|
213
|
+
const detail = (e.stderr || e.message).trim().split('\n').pop();
|
|
214
|
+
return contextFailed(dir, `Clone repo gagal: ${detail}`, commits);
|
|
225
215
|
}
|
|
226
216
|
await revoke(repos, opts.fetch);
|
|
227
217
|
await writeFile(join(dir, 'context.json'), JSON.stringify({
|
|
@@ -231,6 +221,17 @@ export async function auditContext(client, auditId, dir, opts = {}) {
|
|
|
231
221
|
}, null, 2));
|
|
232
222
|
return { ok: true, commits };
|
|
233
223
|
}
|
|
224
|
+
/** Peringatan (annotation di ringkasan run) + context-error.txt untuk audit-submit. */
|
|
225
|
+
async function contextFailed(dir, error, commits) {
|
|
226
|
+
warn(error);
|
|
227
|
+
await mkdir(dir, { recursive: true });
|
|
228
|
+
await writeFile(join(dir, CONTEXT_ERROR_FILE), error);
|
|
229
|
+
return { ok: false, commits, error };
|
|
230
|
+
}
|
|
231
|
+
/** Alasan gagal dari audit-context (context-error.txt di samping context.json), atau null. */
|
|
232
|
+
export async function readContextError(contextFile) {
|
|
233
|
+
return (await readFile(join(dirname(contextFile), CONTEXT_ERROR_FILE), 'utf8').catch(() => '')).trim() || null;
|
|
234
|
+
}
|
|
234
235
|
/** Token GitHub untuk git lewat header dari environment: tidak pernah di argumen, URL remote, atau .git/config. */
|
|
235
236
|
export function gitAuthEnv(token) {
|
|
236
237
|
return {
|
|
@@ -257,14 +258,21 @@ export async function revokeTokens(tokens, doFetch = fetch) {
|
|
|
257
258
|
* Kirim hasil audit. Tanpa hasil terstruktur → audit ditandai gagal dengan penyebab yang mungkin.
|
|
258
259
|
* Screenshot (temuan + galeri halaman) hanya diambil dari dalam folder evidence (path di luar folder itu diabaikan).
|
|
259
260
|
*/
|
|
260
|
-
export async function auditSubmit(client, auditId, claudeOutput, evidenceDir, commits, log = console.log
|
|
261
|
+
export async function auditSubmit(client, auditId, claudeOutput, evidenceDir, commits, log = console.log,
|
|
262
|
+
/** dari audit-context: auditor tidak dijalankan sama sekali */
|
|
263
|
+
contextError = null) {
|
|
261
264
|
const base = `/api/ingest/audits/${encodeURIComponent(auditId)}`;
|
|
262
265
|
const parsed = parseAuditOutput(claudeOutput);
|
|
263
266
|
if (!parsed) {
|
|
264
267
|
log('qa-factory: auditor tidak menghasilkan hasil audit terstruktur');
|
|
268
|
+
// kuota habis / max turns tetap dicatat di pemakaian Claude
|
|
269
|
+
const usage = contextError ? null : readClaudeUsage(claudeOutput);
|
|
265
270
|
return client.post(`${base}/results`, {
|
|
266
271
|
status: 'failed',
|
|
267
|
-
|
|
272
|
+
...(usage ? { usage } : {}),
|
|
273
|
+
error: contextError
|
|
274
|
+
? `${contextError} Cek log step "Ambil konteks & clone repo FE + BE" di repo runner (branch ada? GitHub App Qaify terpasang di repo FE + BE?).`
|
|
275
|
+
: 'Auditor tidak menghasilkan hasil audit. Cek log step "Audit tanpa AI (Playwright)" atau "Audit dengan Claude Code" di repo runner; untuk mode Claude, cek juga secret CLAUDE_CODE_OAUTH_TOKEN dan kuota langganan.',
|
|
268
276
|
commits,
|
|
269
277
|
});
|
|
270
278
|
}
|
package/dist/cli.js
CHANGED
|
@@ -22,7 +22,7 @@ import { appendFile, mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
|
22
22
|
import { dirname } from 'node:path';
|
|
23
23
|
import { INSTRUCTIONS, RESULT_SCHEMA } from './ai.js';
|
|
24
24
|
import { aiQueue, aiSubmit, analyze, CONCLUSIONS, parseArgs } from './analyze.js';
|
|
25
|
-
import { AUDIT_INSTRUCTIONS, AUDIT_PROMPT, AUDIT_SCHEMA, auditContext, auditSubmit, readCommits } from './audit.js';
|
|
25
|
+
import { AUDIT_INSTRUCTIONS, AUDIT_PROMPT, AUDIT_SCHEMA, auditContext, auditSubmit, readCommits, readContextError } from './audit.js';
|
|
26
26
|
import { auditStatic, defaultLaunch, defaultShell, toAuditOutput } from './audit-static.js';
|
|
27
27
|
import { createClient, describeProblem, MISSING_CONFIG, runFromEnv, warn } from './client.js';
|
|
28
28
|
import { FIX_INSTRUCTIONS, FIX_SCHEMA, fixContext, fixDiff, fixSubmit, writeOutputs } from './fix.js';
|
|
@@ -127,6 +127,8 @@ switch (args.command) {
|
|
|
127
127
|
const problem = describeProblem(client);
|
|
128
128
|
if (problem)
|
|
129
129
|
warn(problem);
|
|
130
|
+
// step merah: tanpa ini auditor & kirim hasil dilewati diam-diam dan audit macet "running"
|
|
131
|
+
process.exitCode = 1;
|
|
130
132
|
}
|
|
131
133
|
if (process.env.GITHUB_OUTPUT)
|
|
132
134
|
await appendFile(process.env.GITHUB_OUTPUT, `ok=${result.ok}\n`);
|
|
@@ -170,7 +172,8 @@ switch (args.command) {
|
|
|
170
172
|
// file tidak ada (auditor dilewati / gagal) → audit ditandai gagal
|
|
171
173
|
const output = await readFile(args.results, 'utf8').catch(() => '');
|
|
172
174
|
const commits = args.context ? await readCommits(args.context) : {};
|
|
173
|
-
|
|
175
|
+
const contextError = args.context ? await readContextError(args.context) : null;
|
|
176
|
+
await auditSubmit(client, args.audit, output, args.evidence, commits, console.log, contextError);
|
|
174
177
|
const problem = describeProblem(client);
|
|
175
178
|
if (problem)
|
|
176
179
|
warn(problem);
|
package/dist/fix.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type Exec } from './audit.js';
|
|
2
2
|
import type { IngestClient } from './client.js';
|
|
3
|
+
import { type ClaudeUsage } from './usage.js';
|
|
3
4
|
export declare const FIX_SCHEMA: {
|
|
4
5
|
readonly type: "object";
|
|
5
6
|
readonly properties: {
|
|
@@ -104,13 +105,7 @@ export interface ParsedFix {
|
|
|
104
105
|
summary: string;
|
|
105
106
|
reason: string | null;
|
|
106
107
|
tests_run: string | null;
|
|
107
|
-
usage:
|
|
108
|
-
model: string | null;
|
|
109
|
-
input_tokens: number;
|
|
110
|
-
output_tokens: number;
|
|
111
|
-
num_turns: number | null;
|
|
112
|
-
duration_ms: number | null;
|
|
113
|
-
} | null;
|
|
108
|
+
usage: ClaudeUsage | null;
|
|
114
109
|
}
|
|
115
110
|
/** Output `claude -p --output-format json --json-schema FIX_SCHEMA`; null = tidak ada hasil terstruktur. */
|
|
116
111
|
export declare function parseFixOutput(raw: string): ParsedFix | null;
|
package/dist/fix.js
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
import { appendFile, mkdir, writeFile } from 'node:fs/promises';
|
|
11
11
|
import { join } from 'node:path';
|
|
12
12
|
import { defaultExec, gitAuthEnv, revokeTokens } from './audit.js';
|
|
13
|
+
import { parseClaudeUsage, readClaudeUsage } from './usage.js';
|
|
13
14
|
/** Model untuk --model Claude Code: alias atau ID versi; selain itu ditolak (nilainya masuk ke shell workflow). */
|
|
14
15
|
const MODEL_PATTERN = /^[a-z0-9][a-z0-9.-]{0,63}$/;
|
|
15
16
|
/** Berkas yang tidak boleh diubah perbaikan: workflow (bisa membaca secret repo), rahasia, lockfile. */
|
|
@@ -172,23 +173,12 @@ export function parseFixOutput(raw) {
|
|
|
172
173
|
const out = parsed?.structured_output;
|
|
173
174
|
if (!out || typeof out.fixed !== 'boolean' || typeof out.summary !== 'string')
|
|
174
175
|
return null;
|
|
175
|
-
const models = Object.entries((parsed?.modelUsage ?? {}));
|
|
176
|
-
const model = models.sort(([, a], [, b]) => (b.outputTokens ?? 0) - (a.outputTokens ?? 0))[0]?.[0] ?? null;
|
|
177
|
-
const u = parsed?.usage;
|
|
178
176
|
return {
|
|
179
177
|
fixed: out.fixed,
|
|
180
178
|
summary: out.summary,
|
|
181
179
|
reason: typeof out.reason === 'string' && out.reason ? out.reason : null,
|
|
182
180
|
tests_run: typeof out.tests_run === 'string' && out.tests_run ? out.tests_run : null,
|
|
183
|
-
usage:
|
|
184
|
-
? {
|
|
185
|
-
model,
|
|
186
|
-
input_tokens: Number(u.input_tokens ?? 0) + Number(u.cache_creation_input_tokens ?? 0) + Number(u.cache_read_input_tokens ?? 0),
|
|
187
|
-
output_tokens: Number(u.output_tokens ?? 0),
|
|
188
|
-
num_turns: Number.isInteger(parsed?.num_turns) ? parsed.num_turns : null,
|
|
189
|
-
duration_ms: Number.isInteger(parsed?.duration_ms) ? parsed.duration_ms : null,
|
|
190
|
-
}
|
|
191
|
-
: null,
|
|
181
|
+
usage: parseClaudeUsage(parsed),
|
|
192
182
|
};
|
|
193
183
|
}
|
|
194
184
|
/**
|
|
@@ -207,8 +197,11 @@ export async function fixSubmit(client, fixId, claudeOutput, patch, workDir, opt
|
|
|
207
197
|
};
|
|
208
198
|
const parsed = parseFixOutput(claudeOutput);
|
|
209
199
|
if (!parsed) {
|
|
200
|
+
// kuota habis / max turns tetap dicatat di pemakaian Claude
|
|
201
|
+
const usage = readClaudeUsage(claudeOutput);
|
|
210
202
|
return send({
|
|
211
203
|
status: 'failed',
|
|
204
|
+
...(usage ? { usage } : {}),
|
|
212
205
|
reason: 'Claude tidak menghasilkan hasil perbaikan terstruktur. Cek log step "Perbaiki dengan Claude Code" di repo runner: secret CLAUDE_CODE_OAUTH_TOKEN, kuota langganan, model yang tersedia di paket Anda, atau batas giliran.',
|
|
213
206
|
});
|
|
214
207
|
}
|
package/dist/usage.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export interface ClaudeUsage {
|
|
2
|
+
model: string | null;
|
|
3
|
+
/** termasuk cache (system prompt Claude Code tercatat sebagai cache) */
|
|
4
|
+
input_tokens: number;
|
|
5
|
+
output_tokens: number;
|
|
6
|
+
cache_read_tokens: number;
|
|
7
|
+
/** total_cost_usd: nilai setara harga API, bukan tagihan langganan */
|
|
8
|
+
cost_usd: number | null;
|
|
9
|
+
num_turns: number | null;
|
|
10
|
+
duration_ms: number | null;
|
|
11
|
+
is_error: boolean;
|
|
12
|
+
limit_hit: boolean;
|
|
13
|
+
limit_message: string | null;
|
|
14
|
+
}
|
|
15
|
+
/** Dari objek hasil `claude -p` yang sudah di-parse; null bila tidak ada data pemakaian sama sekali. */
|
|
16
|
+
export declare function parseClaudeUsage(parsed: any): ClaudeUsage | null;
|
|
17
|
+
/** Dari teks mentah claude.json; teks non-JSON yang menyebut limit tetap dilaporkan sebagai limit. */
|
|
18
|
+
export declare function readClaudeUsage(raw: string): ClaudeUsage | null;
|
package/dist/usage.js
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// Pemakaian Claude dari output `claude -p --output-format json`, dikirim ke Qaify untuk panel "Pemakaian Claude".
|
|
2
|
+
// Sisa kuota resmi langganan tidak ada di output ini; yang tercatat hanya pemakaian per panggilan + tanda kena limit.
|
|
3
|
+
// "Claude AI usage limit reached|1762160400", "5-hour limit reached ∙ resets 3pm", "You've hit your limit · resets …"
|
|
4
|
+
const LIMIT_PATTERN = /usage limit|limit reached|hit your (?:usage )?limit|rate[ _-]?limit|resets? (?:at )?\d/i;
|
|
5
|
+
/** Teks limit yang mudah dibaca: epoch setelah "|" diganti waktu reset ISO. */
|
|
6
|
+
function limitMessage(text) {
|
|
7
|
+
return text
|
|
8
|
+
.replace(/\|(\d{10})\b/, (_, epoch) => ` · reset ${new Date(Number(epoch) * 1000).toISOString()}`)
|
|
9
|
+
.trim()
|
|
10
|
+
.slice(0, 500);
|
|
11
|
+
}
|
|
12
|
+
/** Dari objek hasil `claude -p` yang sudah di-parse; null bila tidak ada data pemakaian sama sekali. */
|
|
13
|
+
export function parseClaudeUsage(parsed) {
|
|
14
|
+
if (!parsed || typeof parsed !== 'object')
|
|
15
|
+
return null;
|
|
16
|
+
const isError = parsed.is_error === true || (typeof parsed.subtype === 'string' && parsed.subtype.startsWith('error'));
|
|
17
|
+
const text = typeof parsed.result === 'string' ? parsed.result : '';
|
|
18
|
+
const limitHit = isError && LIMIT_PATTERN.test(text);
|
|
19
|
+
const u = parsed.usage;
|
|
20
|
+
if (!u && !isError)
|
|
21
|
+
return null;
|
|
22
|
+
// modelUsage: { "<model-id>": {...} }; ambil model dengan token keluaran terbanyak
|
|
23
|
+
const models = Object.entries((parsed.modelUsage ?? {}));
|
|
24
|
+
const model = models.sort(([, a], [, b]) => (b.outputTokens ?? 0) - (a.outputTokens ?? 0))[0]?.[0] ?? null;
|
|
25
|
+
return {
|
|
26
|
+
model,
|
|
27
|
+
input_tokens: Number(u?.input_tokens ?? 0) + Number(u?.cache_creation_input_tokens ?? 0) + Number(u?.cache_read_input_tokens ?? 0),
|
|
28
|
+
output_tokens: Number(u?.output_tokens ?? 0),
|
|
29
|
+
cache_read_tokens: Number(u?.cache_read_input_tokens ?? 0),
|
|
30
|
+
cost_usd: typeof parsed.total_cost_usd === 'number' ? parsed.total_cost_usd : null,
|
|
31
|
+
num_turns: Number.isInteger(parsed.num_turns) ? parsed.num_turns : null,
|
|
32
|
+
duration_ms: Number.isInteger(parsed.duration_ms) ? parsed.duration_ms : null,
|
|
33
|
+
is_error: isError,
|
|
34
|
+
limit_hit: limitHit,
|
|
35
|
+
limit_message: limitHit ? limitMessage(text) : null,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
/** Dari teks mentah claude.json; teks non-JSON yang menyebut limit tetap dilaporkan sebagai limit. */
|
|
39
|
+
export function readClaudeUsage(raw) {
|
|
40
|
+
try {
|
|
41
|
+
return parseClaudeUsage(JSON.parse(raw));
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
const text = raw.trim();
|
|
45
|
+
if (!text || !LIMIT_PATTERN.test(text))
|
|
46
|
+
return null;
|
|
47
|
+
return {
|
|
48
|
+
model: null,
|
|
49
|
+
input_tokens: 0,
|
|
50
|
+
output_tokens: 0,
|
|
51
|
+
cache_read_tokens: 0,
|
|
52
|
+
cost_usd: null,
|
|
53
|
+
num_turns: null,
|
|
54
|
+
duration_ms: null,
|
|
55
|
+
is_error: true,
|
|
56
|
+
limit_hit: true,
|
|
57
|
+
limit_message: limitMessage(text.split('\n').find((line) => LIMIT_PATTERN.test(line)) ?? text),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "qaify-playwright-reporter",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.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",
|
|
@@ -106,9 +106,9 @@ jobs:
|
|
|
106
106
|
--output-format json --json-schema "$(cat .qaify/schema.json)" \
|
|
107
107
|
> .qaify/claude.json
|
|
108
108
|
|
|
109
|
-
# selalu jalan: tanpa hasil auditor, audit ditandai gagal beserta
|
|
109
|
+
# selalu jalan (juga saat konteks/clone gagal): tanpa hasil auditor, audit ditandai gagal beserta penyebabnya
|
|
110
110
|
- name: Kirim hasil audit
|
|
111
|
-
if: always()
|
|
111
|
+
if: always()
|
|
112
112
|
env:
|
|
113
113
|
QA_FACTORY_URL: ${{ vars.QA_FACTORY_URL }}
|
|
114
114
|
QA_FACTORY_TOKEN: ${{ secrets.QA_FACTORY_TOKEN }}
|