swipium 1.1.0 → 1.2.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 +15 -9
- package/THREAT_MODEL.md +59 -0
- package/dist/appMap/diff.js +60 -0
- package/dist/appMap/diff.js.map +1 -0
- package/dist/automation/capabilities.js +167 -0
- package/dist/automation/capabilities.js.map +1 -0
- package/dist/automation/gestures.js +94 -0
- package/dist/automation/gestures.js.map +1 -0
- package/dist/automation/maestroIr.js +282 -0
- package/dist/automation/maestroIr.js.map +1 -0
- package/dist/automation/plan.js +268 -0
- package/dist/automation/plan.js.map +1 -0
- package/dist/automation/report.js +83 -0
- package/dist/automation/report.js.map +1 -0
- package/dist/automation/selectors.js +201 -0
- package/dist/automation/selectors.js.map +1 -0
- package/dist/automation/types.js +13 -0
- package/dist/automation/types.js.map +1 -0
- package/dist/automation/waits.js +91 -0
- package/dist/automation/waits.js.map +1 -0
- package/dist/automation/webview.js +81 -0
- package/dist/automation/webview.js.map +1 -0
- package/dist/flows/repair.js +363 -0
- package/dist/flows/repair.js.map +1 -0
- package/dist/interop/maestro.js +151 -0
- package/dist/interop/maestro.js.map +1 -0
- package/dist/mobileAudit/checks.js +251 -0
- package/dist/mobileAudit/checks.js.map +1 -0
- package/dist/mobileAudit/evidence.js +49 -0
- package/dist/mobileAudit/evidence.js.map +1 -0
- package/dist/mobileAudit/profiles.js +73 -0
- package/dist/mobileAudit/profiles.js.map +1 -0
- package/dist/mobileAudit/results.js +41 -0
- package/dist/mobileAudit/results.js.map +1 -0
- package/dist/mobileAudit/runner.js +229 -0
- package/dist/mobileAudit/runner.js.map +1 -0
- package/dist/oracle/locator.js +177 -0
- package/dist/oracle/locator.js.map +1 -0
- package/dist/server.js +20 -0
- package/dist/server.js.map +1 -1
- package/dist/testSuite/exporter.js +70 -0
- package/dist/testSuite/exporter.js.map +1 -0
- package/dist/testSuite/lint.js +53 -0
- package/dist/testSuite/lint.js.map +1 -0
- package/dist/tools/capabilities.js +46 -4
- package/dist/tools/capabilities.js.map +1 -1
- package/dist/tools/flow.js +67 -1
- package/dist/tools/flow.js.map +1 -1
- package/dist/tools/flowRepair.js +66 -0
- package/dist/tools/flowRepair.js.map +1 -0
- package/dist/tools/idling.js +104 -0
- package/dist/tools/idling.js.map +1 -0
- package/dist/tools/inputCapabilities.js +30 -0
- package/dist/tools/inputCapabilities.js.map +1 -0
- package/dist/tools/issues.js +218 -0
- package/dist/tools/issues.js.map +1 -0
- package/dist/tools/jobs.js +12 -1
- package/dist/tools/jobs.js.map +1 -1
- package/dist/tools/locator.js +57 -0
- package/dist/tools/locator.js.map +1 -0
- package/dist/tools/maestro.js +78 -0
- package/dist/tools/maestro.js.map +1 -0
- package/dist/tools/mobileAudit.js +115 -0
- package/dist/tools/mobileAudit.js.map +1 -0
- package/dist/tools/suite.js +49 -1
- package/dist/tools/suite.js.map +1 -1
- package/dist/tools/testSuite.js +233 -0
- package/dist/tools/testSuite.js.map +1 -0
- package/dist/tools/wait.js +58 -0
- package/dist/tools/wait.js.map +1 -0
- package/dist/version.js +31 -2
- package/dist/version.js.map +1 -1
- package/docs/mcp-server.md +1 -1
- package/docs/tools.md +46 -1
- package/package.json +3 -2
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { qaError, qaOk } from '../lib/result.js';
|
|
3
|
+
import { getDriver } from '../session/attach.js';
|
|
4
|
+
import { loadWdaConfig } from '../lib/wdaConfig.js';
|
|
5
|
+
export function registerInputCapabilities(server, sessions) {
|
|
6
|
+
server.registerTool('qa_input_capabilities', {
|
|
7
|
+
title: 'Text input capabilities',
|
|
8
|
+
description: 'Report backend text-entry constraints: ASCII safety, Unicode support, clipboard/paste availability, and WDA typing-frequency tuning. Use before generating fixture-backed form flows.',
|
|
9
|
+
inputSchema: { sessionId: z.string() },
|
|
10
|
+
}, async ({ sessionId }) => {
|
|
11
|
+
const session = sessions.get(sessionId);
|
|
12
|
+
const { driver } = session ? await getDriver(session) : { driver: undefined };
|
|
13
|
+
if (!session || !driver)
|
|
14
|
+
return qaError({ what: 'No device attached to this session', changedState: false, retrySafe: true, nextSteps: ['Prepare a target first.'] });
|
|
15
|
+
const wda = driver.kind === 'wda' ? loadWdaConfig(session.root) : null;
|
|
16
|
+
const caps = {
|
|
17
|
+
backend: driver.kind,
|
|
18
|
+
asciiSafe: true,
|
|
19
|
+
unicodeSupport: driver.kind === 'direct' ? 'limited_android_adb_text' : driver.kind === 'wda' ? 'wda_type_text' : driver.kind === 'remote' ? 'appium_driver_dependent' : 'limited',
|
|
20
|
+
pasteClipboardAvailable: false,
|
|
21
|
+
maxTypingFrequency: typeof wda?.settings.maxTypingFrequency === 'number' ? wda.settings.maxTypingFrequency : null,
|
|
22
|
+
unsupportedFailureCode: 'TEXT_INPUT_UNSUPPORTED',
|
|
23
|
+
policy: driver.kind === 'direct'
|
|
24
|
+
? 'Prefer ASCII fixture values on Android DirectDriver; use declared generators that emit ASCII or a backend with Unicode-safe text entry.'
|
|
25
|
+
: 'Fixture text entry is supported, but keep secrets in variables and tune WDA/Appium settings if typing flakes.',
|
|
26
|
+
};
|
|
27
|
+
return qaOk(caps, `input capabilities: ${driver.kind}; unicode=${caps.unicodeSupport}; clipboard=${caps.pasteClipboardAvailable}`);
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
//# sourceMappingURL=inputCapabilities.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"inputCapabilities.js","sourceRoot":"","sources":["../../src/tools/inputCapabilities.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AACjD,OAAO,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AACjD,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAGpD,MAAM,UAAU,yBAAyB,CAAC,MAAiB,EAAE,QAAsB;IACjF,MAAM,CAAC,YAAY,CACjB,uBAAuB,EACvB;QACE,KAAK,EAAE,yBAAyB;QAChC,WAAW,EAAE,uLAAuL;QACpM,WAAW,EAAE,EAAE,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE,EAAE;KACvC,EACD,KAAK,EAAE,EAAE,SAAS,EAAE,EAAE,EAAE;QACtB,MAAM,OAAO,GAAG,QAAQ,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;QACxC,MAAM,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,CAAC,CAAC,MAAM,SAAS,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,SAAS,EAAE,CAAC;QAC9E,IAAI,CAAC,OAAO,IAAI,CAAC,MAAM;YAAE,OAAO,OAAO,CAAC,EAAE,IAAI,EAAE,oCAAoC,EAAE,YAAY,EAAE,KAAK,EAAE,SAAS,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC,yBAAyB,CAAC,EAAE,CAAC,CAAC;QACtK,MAAM,GAAG,GAAG,MAAM,CAAC,IAAI,KAAK,KAAK,CAAC,CAAC,CAAC,aAAa,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;QACvE,MAAM,IAAI,GAAG;YACX,OAAO,EAAE,MAAM,CAAC,IAAI;YACpB,SAAS,EAAE,IAAI;YACf,cAAc,EAAE,MAAM,CAAC,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,0BAA0B,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,KAAK,KAAK,CAAC,CAAC,CAAC,eAAe,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,yBAAyB,CAAC,CAAC,CAAC,SAAS;YAClL,uBAAuB,EAAE,KAAK;YAC9B,kBAAkB,EAAE,OAAO,GAAG,EAAE,QAAQ,CAAC,kBAAkB,KAAK,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,QAAQ,CAAC,kBAAkB,CAAC,CAAC,CAAC,IAAI;YACjH,sBAAsB,EAAE,wBAAwB;YAChD,MAAM,EAAE,MAAM,CAAC,IAAI,KAAK,QAAQ;gBAC9B,CAAC,CAAC,yIAAyI;gBAC3I,CAAC,CAAC,+GAA+G;SACpH,CAAC;QACF,OAAO,IAAI,CAAC,IAAI,EAAE,uBAAuB,MAAM,CAAC,IAAI,aAAa,IAAI,CAAC,cAAc,eAAe,IAAI,CAAC,uBAAuB,EAAE,CAAC,CAAC;IACrI,CAAC,CACF,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
// SWIPIUM Issue Log MCP tools (SWIPIUM-REQ-07). Expose the durable project issue ledger:
|
|
2
|
+
// qa_issue_log — list current issues + counts + recurrence candidates
|
|
3
|
+
// qa_issue_history — the append-only event trail for one issue
|
|
4
|
+
// qa_issue_mark_fixed — record a fix (date, commit, version, how-fixed) for regression detection
|
|
5
|
+
// qa_issue_triage — change category / severity / owner / note
|
|
6
|
+
// qa_issue_suppress — suppress known noise (stays visible under known-noise, never hidden)
|
|
7
|
+
//
|
|
8
|
+
// Thin wrappers over src/issues/*; heavy logic lives there. Large lists are returned inline but
|
|
9
|
+
// compact (issue ids + summaries), with a resource URI for the full index.
|
|
10
|
+
import { z } from 'zod';
|
|
11
|
+
import { qaOk, qaError } from '../lib/result.js';
|
|
12
|
+
import { resolveProjectRoot } from '../context/projectRoot.js';
|
|
13
|
+
import { markFixed, queryIssues, suppressIssue, triageIssue, verifyFixed, } from '../issues/index.js';
|
|
14
|
+
import { computeIssueMetrics } from '../issues/metrics.js';
|
|
15
|
+
import { issuesResourceUri, readEvents } from '../issues/store.js';
|
|
16
|
+
import { resolveSourceRevision } from '../issues/sourceRevision.js';
|
|
17
|
+
import { loadPolicy } from '../issues/store.js';
|
|
18
|
+
import { ALL_ISSUE_CATEGORIES, ALL_ISSUE_SEVERITIES, ALL_ISSUE_STATES, } from '../issues/schema.js';
|
|
19
|
+
function nowIso() {
|
|
20
|
+
return new Date().toISOString();
|
|
21
|
+
}
|
|
22
|
+
async function rootFor(server, sessions, args) {
|
|
23
|
+
if (args.sessionId) {
|
|
24
|
+
const s = sessions.get(args.sessionId);
|
|
25
|
+
if (!s)
|
|
26
|
+
return { hint: `Unknown sessionId ${args.sessionId}` };
|
|
27
|
+
return { root: s.root };
|
|
28
|
+
}
|
|
29
|
+
const resolved = await resolveProjectRoot(server, args.projectRoot);
|
|
30
|
+
if (!resolved.root)
|
|
31
|
+
return { hint: resolved.hint };
|
|
32
|
+
return { root: resolved.root };
|
|
33
|
+
}
|
|
34
|
+
const KEY_SCHEMA = {
|
|
35
|
+
issueId: z.string().optional().describe('Issue id (iss_…). Provide this OR fingerprint.'),
|
|
36
|
+
fingerprint: z.string().optional().describe('Issue fingerprint (sha256:…). Provide this OR issueId.'),
|
|
37
|
+
};
|
|
38
|
+
export function registerIssues(server, sessions) {
|
|
39
|
+
// --- qa_issue_log ------------------------------------------------------------------------
|
|
40
|
+
server.registerTool('qa_issue_log', {
|
|
41
|
+
title: 'List project issues',
|
|
42
|
+
description: 'List the durable project issue ledger (`.swipium/issues-log.jsonl`). Returns current issue records, counts by state/category/severity, and recurrence candidates (previously-fixed issues seen again). Filter by state, category, severity, platform, or since (ISO date). Suppressed issues are hidden unless includeSuppressed=true (they stay visible under known-noise in reports either way).',
|
|
43
|
+
inputSchema: {
|
|
44
|
+
projectRoot: z.string().optional(),
|
|
45
|
+
sessionId: z.string().optional(),
|
|
46
|
+
state: z.enum(ALL_ISSUE_STATES).optional(),
|
|
47
|
+
category: z.enum(ALL_ISSUE_CATEGORIES).optional(),
|
|
48
|
+
severity: z.enum(ALL_ISSUE_SEVERITIES).optional(),
|
|
49
|
+
platform: z.enum(['ios', 'android', 'web', 'unknown']).optional(),
|
|
50
|
+
since: z.string().optional().describe('ISO timestamp; only issues last seen on/after this.'),
|
|
51
|
+
includeSuppressed: z.boolean().optional(),
|
|
52
|
+
},
|
|
53
|
+
}, async ({ projectRoot, sessionId, state, category, severity, platform, since, includeSuppressed }) => {
|
|
54
|
+
const { root, hint } = await rootFor(server, sessions, { projectRoot, sessionId });
|
|
55
|
+
if (!root)
|
|
56
|
+
return qaError({ what: 'Could not resolve a project root', changedState: false, retrySafe: true, nextSteps: ['Pass projectRoot or sessionId.'], clientHint: hint });
|
|
57
|
+
const query = {
|
|
58
|
+
state: state,
|
|
59
|
+
category: category,
|
|
60
|
+
severity: severity,
|
|
61
|
+
platform: platform,
|
|
62
|
+
since,
|
|
63
|
+
includeSuppressed,
|
|
64
|
+
};
|
|
65
|
+
const res = queryIssues(root, nowIso(), query);
|
|
66
|
+
const compact = res.records.map((r) => ({
|
|
67
|
+
issueId: r.issueId,
|
|
68
|
+
state: r.state,
|
|
69
|
+
category: r.category,
|
|
70
|
+
severity: r.severity,
|
|
71
|
+
summary: r.summary,
|
|
72
|
+
lastSeenAt: r.lastSeenAt,
|
|
73
|
+
observationCount: r.observationCount,
|
|
74
|
+
recurrence: r.lastRecurrenceMessage,
|
|
75
|
+
// REQ-08: linked app-map screens/features, test cases, and fix-verification status.
|
|
76
|
+
linkedScreens: r.appMapRefs?.map((a) => a.screenId).filter(Boolean) ?? [],
|
|
77
|
+
linkedFeatures: r.appMapRefs?.map((a) => a.featureId).filter(Boolean) ?? [],
|
|
78
|
+
linkedTestCases: r.testRefs?.map((t) => t.testCaseId) ?? [],
|
|
79
|
+
verifiedFixedAt: r.lastVerifiedFixedAt,
|
|
80
|
+
verificationStatus: r.state === 'fixed' ? (r.lastVerifiedFixedAt ? 'verified_this_history' : 'unverified') : undefined,
|
|
81
|
+
}));
|
|
82
|
+
return qaOk({ issues: compact, counts: res.counts, recurrenceCandidates: res.recurrenceCandidates.map((r) => r.issueId), resourceUri: issuesResourceUri(root) }, `${res.counts.total} issue(s) — ${Object.entries(res.counts.byState).map(([k, v]) => `${k}=${v}`).join(' ')}${res.recurrenceCandidates.length ? ` | ${res.recurrenceCandidates.length} recurrence candidate(s)` : ''}`);
|
|
83
|
+
});
|
|
84
|
+
// --- qa_issue_history --------------------------------------------------------------------
|
|
85
|
+
server.registerTool('qa_issue_history', {
|
|
86
|
+
title: 'Issue event history',
|
|
87
|
+
description: 'Return the append-only event trail (observed / classified / triaged / fixed / reopened / suppressed) for one issue, so you can audit why it is currently open, fixed, reopened, or suppressed.',
|
|
88
|
+
inputSchema: { projectRoot: z.string().optional(), sessionId: z.string().optional(), ...KEY_SCHEMA },
|
|
89
|
+
}, async ({ projectRoot, sessionId, issueId, fingerprint }) => {
|
|
90
|
+
const { root, hint } = await rootFor(server, sessions, { projectRoot, sessionId });
|
|
91
|
+
if (!root)
|
|
92
|
+
return qaError({ what: 'Could not resolve a project root', changedState: false, retrySafe: true, nextSteps: ['Pass projectRoot or sessionId.'], clientHint: hint });
|
|
93
|
+
if (!issueId && !fingerprint)
|
|
94
|
+
return qaError({ what: 'Provide issueId or fingerprint', changedState: false, retrySafe: true, nextSteps: ['Re-call with issueId or fingerprint (from qa_issue_log).'] });
|
|
95
|
+
const events = readEvents(root).filter((e) => (issueId && e.issueId === issueId) || (fingerprint && e.fingerprint === fingerprint));
|
|
96
|
+
if (events.length === 0)
|
|
97
|
+
return qaError({ what: `No events for ${issueId ?? fingerprint}`, changedState: false, retrySafe: true, nextSteps: ['Check the id via qa_issue_log.'] });
|
|
98
|
+
return qaOk({ issueId: events[0].issueId, fingerprint: events[0].fingerprint, events }, `${events.length} event(s) for ${events[0].issueId}`);
|
|
99
|
+
});
|
|
100
|
+
// --- qa_issue_mark_fixed -----------------------------------------------------------------
|
|
101
|
+
server.registerTool('qa_issue_mark_fixed', {
|
|
102
|
+
title: 'Mark an issue fixed',
|
|
103
|
+
description: 'Record that a developer fixed an issue: appends a `fixed` event with date, commit, version, how-fixed note, and fixedBy, then updates the derived index to state=fixed. Previous observations are NEVER deleted. If the same fingerprint is observed again later, the issue is reopened with a recurrence message that cites this fix metadata.',
|
|
104
|
+
inputSchema: {
|
|
105
|
+
projectRoot: z.string().optional(),
|
|
106
|
+
sessionId: z.string().optional(),
|
|
107
|
+
...KEY_SCHEMA,
|
|
108
|
+
fixedInCommit: z.string().optional().describe('Commit sha the fix landed in. Defaults from sourceRevision/CI env when omitted.'),
|
|
109
|
+
fixedInVersion: z.string().optional(),
|
|
110
|
+
howFixed: z.string().optional().describe('Plain-language note: what changed.'),
|
|
111
|
+
fixedBy: z.string().optional(),
|
|
112
|
+
},
|
|
113
|
+
}, async ({ projectRoot, sessionId, issueId, fingerprint, fixedInCommit, fixedInVersion, howFixed, fixedBy }) => {
|
|
114
|
+
const { root, hint } = await rootFor(server, sessions, { projectRoot, sessionId });
|
|
115
|
+
if (!root)
|
|
116
|
+
return qaError({ what: 'Could not resolve a project root', changedState: false, retrySafe: true, nextSteps: ['Pass projectRoot or sessionId.'], clientHint: hint });
|
|
117
|
+
if (!issueId && !fingerprint)
|
|
118
|
+
return qaError({ what: 'Provide issueId or fingerprint', changedState: false, retrySafe: true, nextSteps: ['Re-call with issueId or fingerprint.'] });
|
|
119
|
+
const policy = loadPolicy(root);
|
|
120
|
+
const sourceRevision = resolveSourceRevision({ env: process.env, allowGitMetadataRead: policy.allowGitMetadataRead, root, explicit: fixedInCommit ? { provider: 'explicit', commit: fixedInCommit } : undefined });
|
|
121
|
+
const res = markFixed(root, { issueId, fingerprint }, { fixedInCommit, fixedInVersion, howFixed, fixedBy, sourceRevision }, nowIso());
|
|
122
|
+
if (!res.ok)
|
|
123
|
+
return qaError({ what: res.reason ?? 'mark-fixed failed', changedState: false, retrySafe: true, nextSteps: ['Confirm the issueId/fingerprint via qa_issue_log.'] });
|
|
124
|
+
return qaOk({ record: res.record, resourceUri: issuesResourceUri(root) }, `marked ${res.record.issueId} fixed${res.record.fixedInCommit ? ` in ${res.record.fixedInCommit}` : ''}`);
|
|
125
|
+
});
|
|
126
|
+
// --- qa_issue_triage ---------------------------------------------------------------------
|
|
127
|
+
server.registerTool('qa_issue_triage', {
|
|
128
|
+
title: 'Triage an issue',
|
|
129
|
+
description: 'Change an issue\'s category, severity, owner, or add a note. Appends a `triaged` event (append-only audit trail).',
|
|
130
|
+
inputSchema: {
|
|
131
|
+
projectRoot: z.string().optional(),
|
|
132
|
+
sessionId: z.string().optional(),
|
|
133
|
+
...KEY_SCHEMA,
|
|
134
|
+
category: z.enum(ALL_ISSUE_CATEGORIES).optional(),
|
|
135
|
+
severity: z.enum(ALL_ISSUE_SEVERITIES).optional(),
|
|
136
|
+
owner: z.enum(['app', 'backend', 'test_env', 'mcp', 'unknown']).optional(),
|
|
137
|
+
note: z.string().optional(),
|
|
138
|
+
},
|
|
139
|
+
}, async ({ projectRoot, sessionId, issueId, fingerprint, category, severity, owner, note }) => {
|
|
140
|
+
const { root, hint } = await rootFor(server, sessions, { projectRoot, sessionId });
|
|
141
|
+
if (!root)
|
|
142
|
+
return qaError({ what: 'Could not resolve a project root', changedState: false, retrySafe: true, nextSteps: ['Pass projectRoot or sessionId.'], clientHint: hint });
|
|
143
|
+
if (!issueId && !fingerprint)
|
|
144
|
+
return qaError({ what: 'Provide issueId or fingerprint', changedState: false, retrySafe: true, nextSteps: ['Re-call with issueId or fingerprint.'] });
|
|
145
|
+
const res = triageIssue(root, { issueId, fingerprint }, { category: category, severity: severity, owner: owner, note }, nowIso());
|
|
146
|
+
if (!res.ok)
|
|
147
|
+
return qaError({ what: res.reason ?? 'triage failed', changedState: false, retrySafe: true, nextSteps: ['Confirm the issueId/fingerprint via qa_issue_log.'] });
|
|
148
|
+
return qaOk({ record: res.record }, `triaged ${res.record.issueId} → ${res.record.category}/${res.record.severity}`);
|
|
149
|
+
});
|
|
150
|
+
// --- qa_issue_suppress -------------------------------------------------------------------
|
|
151
|
+
server.registerTool('qa_issue_suppress', {
|
|
152
|
+
title: 'Suppress known noise',
|
|
153
|
+
description: 'Suppress a known-noise issue with a reason and optional expiration. Suppressed issues are moved OUT of blockers but REMAIN VISIBLE in reports under known-noise — they are never silently hidden.',
|
|
154
|
+
inputSchema: {
|
|
155
|
+
projectRoot: z.string().optional(),
|
|
156
|
+
sessionId: z.string().optional(),
|
|
157
|
+
...KEY_SCHEMA,
|
|
158
|
+
reason: z.string().describe('Why this is known noise.'),
|
|
159
|
+
until: z.string().optional().describe('ISO timestamp the suppression expires.'),
|
|
160
|
+
scope: z.enum(['fingerprint', 'platform', 'environment', 'appVersion']).optional(),
|
|
161
|
+
},
|
|
162
|
+
}, async ({ projectRoot, sessionId, issueId, fingerprint, reason, until, scope }) => {
|
|
163
|
+
const { root, hint } = await rootFor(server, sessions, { projectRoot, sessionId });
|
|
164
|
+
if (!root)
|
|
165
|
+
return qaError({ what: 'Could not resolve a project root', changedState: false, retrySafe: true, nextSteps: ['Pass projectRoot or sessionId.'], clientHint: hint });
|
|
166
|
+
if (!issueId && !fingerprint)
|
|
167
|
+
return qaError({ what: 'Provide issueId or fingerprint', changedState: false, retrySafe: true, nextSteps: ['Re-call with issueId or fingerprint.'] });
|
|
168
|
+
const res = suppressIssue(root, { issueId, fingerprint }, { reason, until, scope: scope }, nowIso());
|
|
169
|
+
if (!res.ok)
|
|
170
|
+
return qaError({ what: res.reason ?? 'suppress failed', changedState: false, retrySafe: true, nextSteps: ['Confirm the issueId/fingerprint via qa_issue_log.'] });
|
|
171
|
+
return qaOk({ record: res.record }, `suppressed ${res.record.issueId} (${reason}) — still visible under known-noise`);
|
|
172
|
+
});
|
|
173
|
+
// --- qa_issue_verify_fixed ---------------------------------------------------------------
|
|
174
|
+
server.registerTool('qa_issue_verify_fixed', {
|
|
175
|
+
title: 'Verify a fixed issue with evidence',
|
|
176
|
+
description: 'Link a passing test/audit result to a FIXED issue and record `verified_fixed` evidence. Requires the issue to be in state "fixed" AND current-run evidence (a reportUri, testCaseId, auditCheckId, or evidenceUris) — a fix cannot be claimed verified without proof. After this, reports may list the issue under "fixed issues verified this run". A reappearance still reopens it with recurrence text.',
|
|
177
|
+
inputSchema: {
|
|
178
|
+
projectRoot: z.string().optional(),
|
|
179
|
+
sessionId: z.string().optional(),
|
|
180
|
+
...KEY_SCHEMA,
|
|
181
|
+
testCaseId: z.string().optional(),
|
|
182
|
+
auditCheckId: z.string().optional(),
|
|
183
|
+
reportUri: z.string().optional(),
|
|
184
|
+
evidenceUris: z.array(z.string()).optional(),
|
|
185
|
+
note: z.string().optional(),
|
|
186
|
+
},
|
|
187
|
+
}, async ({ projectRoot, sessionId, issueId, fingerprint, testCaseId, auditCheckId, reportUri, evidenceUris, note }) => {
|
|
188
|
+
const { root, hint } = await rootFor(server, sessions, { projectRoot, sessionId });
|
|
189
|
+
if (!root)
|
|
190
|
+
return qaError({ what: 'Could not resolve a project root', changedState: false, retrySafe: true, nextSteps: ['Pass projectRoot or sessionId.'], clientHint: hint });
|
|
191
|
+
if (!issueId && !fingerprint)
|
|
192
|
+
return qaError({ what: 'Provide issueId or fingerprint', changedState: false, retrySafe: true, nextSteps: ['Re-call with issueId or fingerprint.'] });
|
|
193
|
+
const res = verifyFixed(root, { issueId, fingerprint }, { testCaseId, auditCheckId, reportUri, evidenceUris, note }, nowIso());
|
|
194
|
+
if (!res.ok)
|
|
195
|
+
return qaError({ what: res.reason ?? 'verify-fixed failed', changedState: false, retrySafe: true, nextSteps: ['Mark the issue fixed first (qa_issue_mark_fixed), and provide current-run evidence.'] });
|
|
196
|
+
return qaOk({ record: res.record }, `verified ${res.record.issueId} fixed with current-run evidence`);
|
|
197
|
+
});
|
|
198
|
+
// --- qa_issue_metrics --------------------------------------------------------------------
|
|
199
|
+
server.registerTool('qa_issue_metrics', {
|
|
200
|
+
title: 'Issue quality metrics',
|
|
201
|
+
description: 'Project-level issue quality metrics derived from the append-only event log: opened/fixed/reopened/verified-fixed counts, issue aging (avg + p95), reopen rate (fixed issues that later reopened), fix-verification rate (fixed issues with verified_fixed evidence), open blocker/high counts, top recurring + aging issues, and a time/version/category series. Answers "is the product improving?".',
|
|
202
|
+
inputSchema: {
|
|
203
|
+
projectRoot: z.string().optional(),
|
|
204
|
+
sessionId: z.string().optional(),
|
|
205
|
+
since: z.string().optional().describe('ISO start of the window.'),
|
|
206
|
+
until: z.string().optional().describe('ISO end of the window (defaults to the latest event).'),
|
|
207
|
+
groupBy: z.enum(['day', 'week', 'version', 'commit', 'category', 'owner', 'screen', 'feature']).optional().describe('Series bucket (default week).'),
|
|
208
|
+
includeSuppressed: z.boolean().optional(),
|
|
209
|
+
},
|
|
210
|
+
}, async ({ projectRoot, sessionId, since, until, groupBy, includeSuppressed }) => {
|
|
211
|
+
const { root, hint } = await rootFor(server, sessions, { projectRoot, sessionId });
|
|
212
|
+
if (!root)
|
|
213
|
+
return qaError({ what: 'Could not resolve a project root', changedState: false, retrySafe: true, nextSteps: ['Pass projectRoot or sessionId.'], clientHint: hint });
|
|
214
|
+
const metrics = computeIssueMetrics(readEvents(root), { since, until, groupBy: groupBy, includeSuppressed });
|
|
215
|
+
return qaOk({ metrics, resourceUri: issuesResourceUri(root) }, `issue metrics: opened=${metrics.opened} fixed=${metrics.fixed} reopened=${metrics.reopened} verifiedFixed=${metrics.verifiedFixed} | reopenRate=${metrics.reopenRatePct}% fixVerify=${metrics.fixVerificationRatePct}% | openBlocker=${metrics.blockerOpenCount} openHigh=${metrics.highOpenCount}`);
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
//# sourceMappingURL=issues.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"issues.js","sourceRoot":"","sources":["../../src/tools/issues.ts"],"names":[],"mappings":"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"}
|
package/dist/tools/jobs.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// qa_job_status — poll a long-running job (boot/install).
|
|
1
|
+
// qa_job_status / qa_job_cancel — poll or cancel a long-running job (boot/install).
|
|
2
2
|
// Lets a client reconnect after a tool-call timeout instead of restarting the work.
|
|
3
3
|
import { z } from 'zod';
|
|
4
4
|
import { qaOk, qaError } from '../lib/result.js';
|
|
@@ -16,5 +16,16 @@ export function registerJobs(server, sessions) {
|
|
|
16
16
|
const progLine = progressLine(job.progressDetail);
|
|
17
17
|
return qaOk({ jobId: job.jobId, kind: job.kind, status: job.status, progress: job.progress, progressDetail: job.progressDetail ?? null, error: job.error, result: job.result, artifactUris: job.artifactUris }, `job ${job.jobId} [${job.kind}] = ${job.status}${progLine ? `\n ${progLine}` : job.progress ? ` (${job.progress})` : ''}${job.resultText ? `\n${job.resultText}` : ''}${job.error ? `\nerror: ${job.error}` : ''}`);
|
|
18
18
|
});
|
|
19
|
+
server.registerTool('qa_job_cancel', {
|
|
20
|
+
title: 'Cancel a job',
|
|
21
|
+
description: 'Cancel a running job; aborts its spawned child processes.',
|
|
22
|
+
inputSchema: { sessionId: z.string(), jobId: z.string() },
|
|
23
|
+
}, async ({ sessionId, jobId }) => {
|
|
24
|
+
const session = sessions.get(sessionId);
|
|
25
|
+
if (!session)
|
|
26
|
+
return qaError({ what: `Unknown sessionId ${sessionId}`, changedState: false, retrySafe: true, nextSteps: ['Call qa_start_session first.'] });
|
|
27
|
+
const ok = sessions.cancelJob(session, jobId);
|
|
28
|
+
return qaOk({ jobId, cancelled: ok }, ok ? `cancelled ${jobId}` : `job ${jobId} not running (already finished or unknown)`);
|
|
29
|
+
});
|
|
19
30
|
}
|
|
20
31
|
//# sourceMappingURL=jobs.js.map
|
package/dist/tools/jobs.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"jobs.js","sourceRoot":"","sources":["../../src/tools/jobs.ts"],"names":[],"mappings":"AAAA,
|
|
1
|
+
{"version":3,"file":"jobs.js","sourceRoot":"","sources":["../../src/tools/jobs.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// qa_locator_suggest (NEXT-PLAN: Locator And Maintainability) — for the current screen, recommend
|
|
2
|
+
// the most durable locator for each element + score it, and grade the screen's "automation
|
|
3
|
+
// readiness" (how much of the interactive UI has a durable testID/accessibility handle). This
|
|
4
|
+
// tells a developer exactly which controls need testIDs to make flows non-brittle.
|
|
5
|
+
import { z } from 'zod';
|
|
6
|
+
import { qaOk, qaError } from '../lib/result.js';
|
|
7
|
+
import { parseSnapshot, signature } from '../snapshot/parse.js';
|
|
8
|
+
import { suggestLocator, automationReadiness, locatorReadinessIssues } from '../oracle/locator.js';
|
|
9
|
+
import { getDriver } from '../session/attach.js';
|
|
10
|
+
export function registerLocator(server, sessions) {
|
|
11
|
+
server.registerTool('qa_locator_suggest', {
|
|
12
|
+
title: 'Suggest durable locators',
|
|
13
|
+
description: 'For the current screen, recommend the most durable locator for each element (priority: accessibility/content-desc > resource-id/testID > visible text > structure > coordinate) with a durability score, and grade the screen\'s automation readiness — listing exactly which interactive controls need a testID. Use this to make generated flows non-brittle and to hand developers an actionable testID to-do list. Needs a structured UI tree (not the iOS simulator / visual-fallback).',
|
|
14
|
+
inputSchema: {
|
|
15
|
+
sessionId: z.string(),
|
|
16
|
+
interactiveOnly: z.boolean().optional().describe('Only score clickable elements (default false → all addressable elements).'),
|
|
17
|
+
},
|
|
18
|
+
}, async ({ sessionId, interactiveOnly }) => {
|
|
19
|
+
const session = sessions.get(sessionId);
|
|
20
|
+
const { driver } = session ? await getDriver(session) : { driver: undefined };
|
|
21
|
+
if (!session || !driver) {
|
|
22
|
+
return qaError({ what: 'No device attached to this session', changedState: false, retrySafe: true, nextSteps: ['Call qa_prepare_target first.'] });
|
|
23
|
+
}
|
|
24
|
+
if (driver.kind === 'simulator' || session.mode === 'visual-fallback') {
|
|
25
|
+
return qaError({
|
|
26
|
+
what: 'Locator suggestions need a structured UI tree, which this backend/screen lacks',
|
|
27
|
+
changedState: false,
|
|
28
|
+
retrySafe: false,
|
|
29
|
+
failureCode: 'BACKEND_UNSUPPORTED',
|
|
30
|
+
nextSteps: ['On the iOS simulator or a visual-only screen there is no a11y tree to score. Use qa_visual / qa_assert_visual instead.'],
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
let xml;
|
|
34
|
+
try {
|
|
35
|
+
xml = await driver.dumpXml();
|
|
36
|
+
}
|
|
37
|
+
catch (e) {
|
|
38
|
+
return qaError({ what: `Could not read the UI tree: ${String(e)}`, changedState: false, retrySafe: true, failureCode: 'SNAPSHOT_FAILED', nextSteps: ['Retry on a settled screen.'] });
|
|
39
|
+
}
|
|
40
|
+
const parsed = parseSnapshot(xml, { interactiveOnly: interactiveOnly ?? false });
|
|
41
|
+
session.lastSnapshot = { fullByRef: parsed.fullByRef, signatures: new Set(parsed.elements.map(signature)), allNodes: parsed.allNodes };
|
|
42
|
+
const platform = driver.kind === 'wda' ? 'ios' : driver.kind === 'direct' || driver.kind === 'remote' ? 'android' : 'generic';
|
|
43
|
+
const suggestions = parsed.elements.map((el) => suggestLocator(el, { platform }));
|
|
44
|
+
const readiness = automationReadiness(suggestions, parsed.elements, { platform });
|
|
45
|
+
const issues = locatorReadinessIssues(suggestions, parsed.elements, { platform });
|
|
46
|
+
const tierTally = suggestions.reduce((a, s) => ((a[s.tier] = (a[s.tier] ?? 0) + 1), a), {});
|
|
47
|
+
const summary = `automation readiness: ${readiness.grade} (${readiness.durablePct}% of ${readiness.interactiveCount} interactive controls have a durable ${platform === 'ios' ? 'accessibilityIdentifier' : 'testID/a11y'} handle)\n` +
|
|
48
|
+
`platform: ${platform}\n` +
|
|
49
|
+
`locator tiers: ${Object.entries(tierTally).map(([t, n]) => `${t}=${n}`).join(' ')}\n` +
|
|
50
|
+
(issues.length ? `issues:\n${issues.slice(0, 8).map((i) => ` - ${i.code}: ${i.message}`).join('\n')}\n` : '') +
|
|
51
|
+
(readiness.needTestIds.length
|
|
52
|
+
? `needs a testID (${readiness.needTestIds.length}):\n` + readiness.needTestIds.slice(0, 15).map((n) => ` - ${n.ref} [${n.role}] — ${n.hint}${n.suggestedTestId ? `; suggested ${platform === 'ios' ? 'accessibilityIdentifier' : 'testID'}: ${n.suggestedTestId}` : ''}`).join('\n')
|
|
53
|
+
: 'every interactive control already has a durable locator ✅');
|
|
54
|
+
return qaOk({ platform, readiness, suggestions, tierTally, issues }, summary);
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
//# sourceMappingURL=locator.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"locator.js","sourceRoot":"","sources":["../../src/tools/locator.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { qaError, qaOk } from '../lib/result.js';
|
|
4
|
+
import { importMaestro } from '../interop/maestro.js';
|
|
5
|
+
import { importMaestroActions, exportMaestroActions } from '../automation/maestroIr.js';
|
|
6
|
+
import { compileActions } from '../automation/plan.js';
|
|
7
|
+
import { parseFlow } from '../flows/schema.js';
|
|
8
|
+
function rootFor(sessions, sessionId, projectRoot) {
|
|
9
|
+
return sessionId ? sessions.get(sessionId)?.root : projectRoot;
|
|
10
|
+
}
|
|
11
|
+
export function registerMaestro(server, sessions) {
|
|
12
|
+
server.registerTool('qa_maestro_import', {
|
|
13
|
+
title: 'Import Maestro YAML',
|
|
14
|
+
description: 'Import Maestro commands into the Automation Kernel V2 Action IR (`actionIr`), preserving explicit tapOn retryTapIfNoChange/repeat/settle, longPressOn, full scrollUntilVisible options, waitForAnimationToEnd, assertNotVisible, and eraseText. `unsupported` + `grades` are the authoritative semantic view (a command is unsupported only if it cannot be automated at all — e.g. clearState, repeat loops). Also emits `flowYaml` (the narrower Flow V2 subset for a saved .swipium/flows artifact) and `flowYamlUnsupported` (commands the IR supports but the Flow V2 YAML cannot yet express, e.g. longPressOn/assertNotVisible).',
|
|
15
|
+
inputSchema: {
|
|
16
|
+
sessionId: z.string().optional(),
|
|
17
|
+
projectRoot: z.string().optional(),
|
|
18
|
+
maestroYaml: z.string().optional(),
|
|
19
|
+
path: z.string().optional(),
|
|
20
|
+
name: z.string().optional(),
|
|
21
|
+
appId: z.string().optional(),
|
|
22
|
+
},
|
|
23
|
+
}, async ({ sessionId, projectRoot, maestroYaml, path, name, appId }) => {
|
|
24
|
+
const root = rootFor(sessions, sessionId, projectRoot);
|
|
25
|
+
if (!maestroYaml && !path)
|
|
26
|
+
return qaError({ what: 'Pass maestroYaml or path', changedState: false, retrySafe: true, nextSteps: ['Provide the Maestro YAML content or file path.'] });
|
|
27
|
+
try {
|
|
28
|
+
const text = maestroYaml ?? readFileSync(path, 'utf8');
|
|
29
|
+
const legacy = importMaestro(text, { name, appId });
|
|
30
|
+
const ir = importMaestroActions(text);
|
|
31
|
+
const session = sessionId ? sessions.get(sessionId) : undefined;
|
|
32
|
+
const uri = session ? sessions.saveArtifact(session, 'flow', `maestro-import-${Date.now()}.yaml`, legacy.flowYaml, 'text/yaml', 'Maestro import as Swipium Flow V2') : undefined;
|
|
33
|
+
return qaOk({
|
|
34
|
+
// V2 Action IR is authoritative: longPressOn/assertNotVisible/etc are supported here and
|
|
35
|
+
// are NOT counted as unsupported (which was the contradiction the legacy importer caused).
|
|
36
|
+
actionIr: ir.actions,
|
|
37
|
+
grades: ir.grades,
|
|
38
|
+
unsupported: ir.unsupported,
|
|
39
|
+
// Flow V2 YAML (for a saved artifact) + the commands it cannot yet express even though the IR can.
|
|
40
|
+
flowYaml: legacy.flowYaml,
|
|
41
|
+
flowYamlUnsupported: legacy.unsupported,
|
|
42
|
+
artifactUri: uri ?? null,
|
|
43
|
+
root: root ?? null,
|
|
44
|
+
}, `imported Maestro → ${ir.actions.length} Action IR step(s), ${ir.unsupported.length} unsupported${legacy.unsupported.length ? `, ${legacy.unsupported.length} not expressible in Flow V2 YAML` : ''}${uri ? `\nflow: ${uri}` : ''}`);
|
|
45
|
+
}
|
|
46
|
+
catch (e) {
|
|
47
|
+
return qaError({ what: `Could not import Maestro YAML: ${String(e.message ?? e)}`, changedState: false, retrySafe: true, nextSteps: ['Check the YAML syntax and supported command subset.'] });
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
server.registerTool('qa_maestro_export', {
|
|
51
|
+
title: 'Export Swipium Flow to Maestro',
|
|
52
|
+
description: 'Export a Swipium Flow V2 YAML to Maestro YAML via the Automation Kernel V2 IR exporter. Native selectors map to Maestro selector blocks (never XPath). Steps with no portable Maestro form — iOS predicate/class-chain selectors, visual/OCR/diff, and other Swipium/Appium/WDA-only steps — are emitted as explicit `manualReview` entries graded manual_review_required, never silently downgraded.',
|
|
53
|
+
inputSchema: {
|
|
54
|
+
sessionId: z.string().optional(),
|
|
55
|
+
flowYaml: z.string().optional(),
|
|
56
|
+
path: z.string().optional(),
|
|
57
|
+
},
|
|
58
|
+
}, async ({ sessionId, flowYaml, path }) => {
|
|
59
|
+
if (!flowYaml && !path)
|
|
60
|
+
return qaError({ what: 'Pass flowYaml or path', changedState: false, retrySafe: true, nextSteps: ['Provide Swipium flow YAML content or a path.'] });
|
|
61
|
+
try {
|
|
62
|
+
const text = flowYaml ?? readFileSync(path, 'utf8');
|
|
63
|
+
const { flow: parsed, errors } = parseFlow(text);
|
|
64
|
+
if (errors.length || !parsed) {
|
|
65
|
+
return qaError({ what: `Flow is invalid (${errors.length} error${errors.length === 1 ? '' : 's'}) — run qa_flow_check`, changedState: false, retrySafe: true, nextSteps: ['Fix the flow YAML and re-export.'] }, { errors });
|
|
66
|
+
}
|
|
67
|
+
const result = exportMaestroActions(compileActions(parsed));
|
|
68
|
+
const session = sessionId ? sessions.get(sessionId) : undefined;
|
|
69
|
+
const uri = session ? sessions.saveArtifact(session, 'maestro', `maestro-export-${Date.now()}.yaml`, result.maestroYaml, 'text/yaml', 'Swipium Flow exported to Maestro') : undefined;
|
|
70
|
+
const portable = result.grades.filter((g) => g.grade === 'portable' || g.grade === 'maestro_supported').length;
|
|
71
|
+
return qaOk({ maestroYaml: result.maestroYaml, grades: result.grades, unsupported: result.unsupported, artifactUri: uri ?? null }, `exported Maestro YAML: ${portable}/${result.grades.length} portable, ${result.unsupported.length} need manual review${uri ? `\nartifact: ${uri}` : ''}`);
|
|
72
|
+
}
|
|
73
|
+
catch (e) {
|
|
74
|
+
return qaError({ what: `Could not export Maestro YAML: ${String(e.message ?? e)}`, changedState: false, retrySafe: true, nextSteps: ['Check the Swipium Flow YAML.'] });
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
//# sourceMappingURL=maestro.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"maestro.js","sourceRoot":"","sources":["../../src/tools/maestro.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
// SWIPIUM Mobile QA Toolkit MCP tool (SWIPIUM-REQ-07). qa_mobile_audit turns a named profile
|
|
2
|
+
// (smoke | account_cycle | store_compliance | resilience | release_gate) into an ordered, classified
|
|
3
|
+
// checklist plus the account-cycle safety contract the agent executes with the existing driver tools.
|
|
4
|
+
// It also surfaces the issue-ledger recurrence state so a release gate sees regressions. Thin wrapper
|
|
5
|
+
// over src/mobileAudit/* and src/issues/*.
|
|
6
|
+
import { z } from 'zod';
|
|
7
|
+
import { qaOk, qaError } from '../lib/result.js';
|
|
8
|
+
import { resolveProjectRoot } from '../context/projectRoot.js';
|
|
9
|
+
import { getDriver } from '../session/attach.js';
|
|
10
|
+
import { accountCycleSafety, checksForProfile, ALL_PROFILES } from '../mobileAudit/profiles.js';
|
|
11
|
+
import { runMobileAudit } from '../mobileAudit/runner.js';
|
|
12
|
+
import { auditRunToMarkdown } from '../mobileAudit/results.js';
|
|
13
|
+
import { generateSessionReport } from '../services/report.js';
|
|
14
|
+
import { queryIssues } from '../issues/index.js';
|
|
15
|
+
import { issuesResourceUri } from '../issues/store.js';
|
|
16
|
+
import { resolveSourceRevision } from '../issues/sourceRevision.js';
|
|
17
|
+
import { loadPolicy } from '../issues/store.js';
|
|
18
|
+
function nowIso() {
|
|
19
|
+
return new Date().toISOString();
|
|
20
|
+
}
|
|
21
|
+
async function rootFor(server, sessions, args) {
|
|
22
|
+
if (args.sessionId) {
|
|
23
|
+
const s = sessions.get(args.sessionId);
|
|
24
|
+
if (!s)
|
|
25
|
+
return { hint: `Unknown sessionId ${args.sessionId}` };
|
|
26
|
+
return { root: s.root };
|
|
27
|
+
}
|
|
28
|
+
const resolved = await resolveProjectRoot(server, args.projectRoot);
|
|
29
|
+
if (!resolved.root)
|
|
30
|
+
return { hint: resolved.hint };
|
|
31
|
+
return { root: resolved.root };
|
|
32
|
+
}
|
|
33
|
+
export function registerMobileAudit(server, sessions) {
|
|
34
|
+
server.registerTool('qa_mobile_audit', {
|
|
35
|
+
title: 'Mobile QA release audit',
|
|
36
|
+
description: 'Run a named mobile-QA audit profile in one of two modes. mode:"plan" (default) returns the ordered checklist (each with expected behavior + report classification), the account-cycle safety contract, and an `executor` handoff — it does NOT drive the device. mode:"execute" REQUIRES a sessionId with an attached device and DRIVES the audit: it runs each check, records issue-ledger entries with evidence for fail/blocked checks, returns per-check MobileAuditCheckResults + a release-impact decision, and (best-effort) a report URI. Profiles: smoke (launch/health/nav), account_cycle (create/logout/login-again/forgot-password on a DISPOSABLE generated account — needs allowGeneratedData), store_compliance (privacy/terms/account-deletion/subscription/paywall), resilience (offline/restore/relaunch/rotation — network is always restored), release_gate (all profiles + locator readiness + issue-ledger recurrence). Safety: logout is permitted only inside the controlled account-cycle workflow on a disposable account; delete/pay/send stay refused; a real account is never deleted; OTP/MFA returns needs_input; a check never reports pass without observed evidence.',
|
|
37
|
+
inputSchema: {
|
|
38
|
+
projectRoot: z.string().optional(),
|
|
39
|
+
sessionId: z.string().optional(),
|
|
40
|
+
profile: z.enum(ALL_PROFILES).describe('Audit profile to run.'),
|
|
41
|
+
mode: z.enum(['plan', 'execute']).optional().describe('plan (default): return the checklist + safety contract without driving the device. execute: drive the audit against the attached device, record issues + evidence, and return per-check results + release impact.'),
|
|
42
|
+
allowGeneratedData: z.boolean().optional().describe('Permit safe generated disposable-account data (test/staging).'),
|
|
43
|
+
allowTestAccountDeletion: z.boolean().optional().describe('Permit destructive cleanup of disposable test accounts only.'),
|
|
44
|
+
offlineMode: z.boolean().optional().describe('Hint that resilience checks should drive offline state.'),
|
|
45
|
+
sourceRevision: z.object({ commit: z.string().optional(), buildVersion: z.string().optional(), branch: z.string().optional() }).optional(),
|
|
46
|
+
targetApp: z.string().optional(),
|
|
47
|
+
waitForCompletion: z.boolean().optional().describe('Reserved; execute currently always runs to completion and returns the full result.'),
|
|
48
|
+
},
|
|
49
|
+
}, async ({ projectRoot, sessionId, profile, mode, allowGeneratedData, allowTestAccountDeletion, offlineMode, sourceRevision, targetApp }) => {
|
|
50
|
+
const { root, hint } = await rootFor(server, sessions, { projectRoot, sessionId });
|
|
51
|
+
if (!root)
|
|
52
|
+
return qaError({ what: 'Could not resolve a project root', changedState: false, retrySafe: true, nextSteps: ['Pass projectRoot or sessionId.'], clientHint: hint });
|
|
53
|
+
const prof = profile;
|
|
54
|
+
const policyForRev = loadPolicy(root);
|
|
55
|
+
const revisionResolved = resolveSourceRevision({
|
|
56
|
+
explicit: sourceRevision ? { provider: 'explicit', ...sourceRevision } : undefined,
|
|
57
|
+
env: process.env,
|
|
58
|
+
allowGitMetadataRead: policyForRev.allowGitMetadataRead,
|
|
59
|
+
root,
|
|
60
|
+
});
|
|
61
|
+
// ---- EXECUTE: drive the audit against the attached device ----
|
|
62
|
+
if (mode === 'execute') {
|
|
63
|
+
const session = sessionId ? sessions.get(sessionId) : undefined;
|
|
64
|
+
if (!session)
|
|
65
|
+
return qaError({ what: 'execute mode needs a sessionId with a prepared device', changedState: false, retrySafe: true, nextSteps: ['Call qa_start_session + qa_prepare_target (or qa_test_this), then qa_mobile_audit { mode:"execute" }.'] });
|
|
66
|
+
const { driver } = await getDriver(session);
|
|
67
|
+
if (!driver)
|
|
68
|
+
return qaError({ what: 'No device attached to this session', changedState: false, retrySafe: true, nextSteps: ['Prepare a device first (qa_test_this / qa_prepare_target), then re-run.'] });
|
|
69
|
+
try {
|
|
70
|
+
const run = await runMobileAudit(sessions, session, driver, { profile: prof, allowGeneratedData, allowTestAccountDeletion, offlineMode, sourceRevision: revisionResolved, now: nowIso() });
|
|
71
|
+
let reportUri;
|
|
72
|
+
try {
|
|
73
|
+
const r = await generateSessionReport(sessions, session, {});
|
|
74
|
+
reportUri = r.reportUri;
|
|
75
|
+
run.reportUri = reportUri;
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
/* report is best-effort */
|
|
79
|
+
}
|
|
80
|
+
const passed = run.checks.filter((c) => c.status === 'pass').length;
|
|
81
|
+
return qaOk({ ...run, markdown: auditRunToMarkdown(run), resourceUri: issuesResourceUri(root) }, `🔍 mobile audit ${prof}: ${run.state}, release=${run.releaseImpact} — ${passed}/${run.checks.length} pass, ${run.issueIds.length} issue(s)${run.recurrenceWarnings.length ? `, ${run.recurrenceWarnings.length} recurrence` : ''}`);
|
|
82
|
+
}
|
|
83
|
+
catch (e) {
|
|
84
|
+
return qaError({ what: `Mobile audit failed: ${String(e.message ?? e)}`, changedState: true, retrySafe: true, nextSteps: ['Check device health (qa_check_health) and retry.'] });
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
// ---- PLAN (default) ----
|
|
88
|
+
const checks = checksForProfile(prof);
|
|
89
|
+
const safety = accountCycleSafety(prof, { allowGeneratedData, allowTestAccountDeletion, offlineMode });
|
|
90
|
+
// qa_mobile_audit PLANS the audit; the controlled account-cycle steps (logout permitted only on
|
|
91
|
+
// a disposable account) are EXECUTED by qa_explore with accountCycle:true + allowGeneratedData.
|
|
92
|
+
const executor = safety.allowsLogout
|
|
93
|
+
? { tool: 'qa_explore', args: { accountCycle: true, allowGeneratedData: allowGeneratedData === true }, note: 'Runs the account-cycle workflow where logout is permitted on a disposable account; delete/pay/send stay refused.' }
|
|
94
|
+
: undefined;
|
|
95
|
+
// Account-cycle / release-gate without generated-data permission can't safely create accounts.
|
|
96
|
+
const blockedChecks = checks.filter((c) => c.requiresGeneratedData && !allowGeneratedData).map((c) => c.id);
|
|
97
|
+
// Surface issue-ledger recurrence so a release gate sees regressions.
|
|
98
|
+
const issues = queryIssues(root, nowIso(), { includeSuppressed: false });
|
|
99
|
+
const recurrence = issues.recurrenceCandidates.map((r) => ({ issueId: r.issueId, summary: r.summary, message: r.lastRecurrenceMessage }));
|
|
100
|
+
const summary = `audit profile=${prof}: ${checks.length} check(s)${blockedChecks.length ? `, ${blockedChecks.length} need allowGeneratedData` : ''}${recurrence.length ? `, ${recurrence.length} recurrence warning(s)` : ''}`;
|
|
101
|
+
return qaOk({
|
|
102
|
+
mode: 'plan',
|
|
103
|
+
profile: prof,
|
|
104
|
+
targetApp,
|
|
105
|
+
checks,
|
|
106
|
+
accountCycleSafety: safety,
|
|
107
|
+
executor,
|
|
108
|
+
blockedChecks,
|
|
109
|
+
recurrenceWarnings: recurrence,
|
|
110
|
+
sourceRevision: revisionResolved,
|
|
111
|
+
resourceUri: issuesResourceUri(root),
|
|
112
|
+
}, summary);
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
//# sourceMappingURL=mobileAudit.js.map
|