@aarwitz/tapp 0.16.4 โ 0.17.0-rc.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/AGENTS.md +26 -17
- package/Harness/OCQAHarnessUITests/ExplorerTests.swift +1 -1
- package/README.md +69 -63
- package/bin/tapp.js +102 -37
- package/browser/app.js +12 -6
- package/docs/BROWSER-PRODUCT.md +75 -0
- package/docs/PRODUCT-ENGINE.md +107 -0
- package/docs/application-model.md +271 -0
- package/docs/scenarios.md +95 -0
- package/mcp-server/src/application-model.js +9 -8
- package/mcp-server/src/browser-product.js +1 -1
- package/mcp-server/src/ci-report.js +84 -53
- package/mcp-server/src/ci-setup.js +35 -5
- package/mcp-server/src/enrich.js +1 -1
- package/mcp-server/src/html-report.js +4 -3
- package/mcp-server/src/index.js +142 -43
- package/mcp-server/src/product-execution.js +1 -1
- package/mcp-server/src/product-operations.js +2 -2
- package/mcp-server/src/project-config.js +1 -2
- package/mcp-server/src/project-paths.js +5 -17
- package/mcp-server/src/release-contract.js +3 -3
- package/mcp-server/src/report.js +157 -31
- package/mcp-server/src/task-runtime.js +1 -1
- package/package.json +2 -2
- package/scripts/ci-gate.sh +3 -3
- package/scripts/quick-capture.sh +1 -1
- package/scripts/run-flow.sh +1 -1
package/mcp-server/src/report.js
CHANGED
|
@@ -96,14 +96,41 @@ export const ISSUE_CATEGORY = {
|
|
|
96
96
|
explore_timeout: "performance_timeout",
|
|
97
97
|
};
|
|
98
98
|
export const CRITICAL_ISSUE_TYPES = new Set(["crash"]);
|
|
99
|
+
export const WEB_SAMPLED_ISSUE_TYPES = new Set(["unresponsive_element"]);
|
|
99
100
|
|
|
100
101
|
export function severityRank(s) {
|
|
101
102
|
return { critical: 0, high: 1, medium: 2, low: 3 }[s] ?? 4;
|
|
102
103
|
}
|
|
103
104
|
|
|
104
|
-
export function
|
|
105
|
-
|
|
106
|
-
|
|
105
|
+
export function findingEvaluationTier(finding, platform = "ios") {
|
|
106
|
+
return platform === "web" && WEB_SAMPLED_ISSUE_TYPES.has(finding?.type) ? "sampled" : "deterministic";
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// The deterministic block-by-findings rule, expressed over verdict-tier finding COUNTS โ not the
|
|
110
|
+
// score scalar and not the `verdict` label โ so the CI gate survives removal of verdict/releaseScore
|
|
111
|
+
// from exploration output. It encodes exactly what `verdict === "blocked"` used to: a critical
|
|
112
|
+
// finding always blocks; otherwise, on a CONCLUSIVE run, a risk threshold blocks. The risk threshold
|
|
113
|
+
// is kept as an explicit, chosen rule (ADR-0005) โ `riskFromCounts` is the single source of that
|
|
114
|
+
// formula, shared with buildQaReport's verdict label. Inconclusive runs are a separate gate outcome,
|
|
115
|
+
// never a findings-block, so a thin run reports `inconclusive`, not `fail`.
|
|
116
|
+
export function riskFromCounts({ critical = 0, high = 0, medium = 0 } = {}) {
|
|
117
|
+
return Math.max(0, Math.min(100, 100 - critical * 25 - high * 10 - medium * 3));
|
|
118
|
+
}
|
|
119
|
+
export function findingsBlock(deterministicFindingCounts = {}, { inconclusive = false } = {}) {
|
|
120
|
+
if ((deterministicFindingCounts.critical || 0) > 0) return true;
|
|
121
|
+
if (inconclusive) return false;
|
|
122
|
+
return riskFromCounts(deterministicFindingCounts) < 50;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Exploration OBSERVES; it never renders a ship verdict or score (ADR-0005). These label the
|
|
126
|
+
// observation honestly. The release decision (pass/fail/inconclusive) is the gate's, shown separately.
|
|
127
|
+
export function observationBadge(report) {
|
|
128
|
+
return report?.inconclusive ? "๐ก INCONCLUSIVE (exploration)" : "๐ญ EXPLORED";
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function observationSummary(report) {
|
|
132
|
+
const n = report?.findingCounts?.total || 0;
|
|
133
|
+
return `${report?.screensExplored || 0} screens ยท ${report?.actionsPerformed || 0} actions ยท ${n} finding(s) ยท observation only`;
|
|
107
134
|
}
|
|
108
135
|
|
|
109
136
|
// Turn a capture's OCQA markers into the same ship/no-ship report Tapp produces:
|
|
@@ -186,7 +213,15 @@ export function buildQaReport(markersFilePath, { platform = "ios" } = {}) {
|
|
|
186
213
|
const key = `${i.type}|${i.screen}|${i.target ?? ""}`;
|
|
187
214
|
if (seen.has(key)) continue;
|
|
188
215
|
seen.add(key);
|
|
189
|
-
findings.push({
|
|
216
|
+
findings.push({
|
|
217
|
+
...i,
|
|
218
|
+
category: ISSUE_CATEGORY[i.type] || i.type,
|
|
219
|
+
// Structural evidence authority (ADR-0005): marker-derived findings are deterministic. The
|
|
220
|
+
// default gate consumes only deterministic-authority evidence; model-observed findings
|
|
221
|
+
// (vision/assert_ai) carry authority:"model-observed" and are advisory, never gate fails.
|
|
222
|
+
authority: "deterministic",
|
|
223
|
+
...(platform === "web" ? { evaluationTier: findingEvaluationTier(i, platform) } : {}),
|
|
224
|
+
});
|
|
190
225
|
}
|
|
191
226
|
findings.sort((a, b) => severityRank(a.severity) - severityRank(b.severity));
|
|
192
227
|
|
|
@@ -197,28 +232,27 @@ export function buildQaReport(markersFilePath, { platform = "ios" } = {}) {
|
|
|
197
232
|
const high = findings.filter((f) => f.severity === "high").length;
|
|
198
233
|
const med = findings.filter((f) => f.severity === "medium").length;
|
|
199
234
|
const low = findings.filter((f) => f.severity === "low").length;
|
|
235
|
+
const verdictFindings = platform === "web"
|
|
236
|
+
? findings.filter((finding) => finding.evaluationTier !== "sampled")
|
|
237
|
+
: findings;
|
|
238
|
+
const verdictCrit = verdictFindings.filter((f) => f.severity === "critical").length;
|
|
239
|
+
const verdictHigh = verdictFindings.filter((f) => f.severity === "high").length;
|
|
240
|
+
const verdictMed = verdictFindings.filter((f) => f.severity === "medium").length;
|
|
241
|
+
const verdictLow = verdictFindings.filter((f) => f.severity === "low").length;
|
|
242
|
+
const sampledFindings = platform === "web" ? findings.filter((finding) => finding.evaluationTier === "sampled") : [];
|
|
200
243
|
|
|
201
|
-
// Coverage floor:
|
|
244
|
+
// Coverage floor: exploration is inconclusive if the app wasn't actually exercised. Exploration
|
|
245
|
+
// OBSERVES โ it does not render a ship verdict or score (ADR-0005). Judgment (pass/fail/
|
|
246
|
+
// inconclusive) is the gate's job (evaluateGate), computed from these findings + coverage + policy.
|
|
202
247
|
const inconclusive = screensExplored < 2 || actionsPerformed < 3;
|
|
203
|
-
let confidence = Math.max(0, Math.min(100, 100 - crit * 25 - high * 10 - med * 3));
|
|
204
|
-
if (inconclusive) confidence = Math.min(confidence, 40);
|
|
205
|
-
|
|
206
|
-
let verdict;
|
|
207
|
-
if (crit > 0) verdict = "blocked";
|
|
208
|
-
else if (inconclusive) verdict = "caution";
|
|
209
|
-
else if (confidence < 50) verdict = "blocked";
|
|
210
|
-
else if (high > 0 || confidence < 80) verdict = "caution";
|
|
211
|
-
else verdict = "ready";
|
|
212
248
|
|
|
213
249
|
const headline = inconclusive
|
|
214
250
|
? `Inconclusive โ only ${screensExplored} screen(s) / ${actionsPerformed} action(s) explored. The app may have crashed on launch, be stuck behind a sign-in wall, or otherwise prevent exploration. Absence of issues is NOT a pass.`
|
|
215
|
-
:
|
|
251
|
+
: findings.length === 0
|
|
216
252
|
? platform === "web"
|
|
217
|
-
? "Automated web checks
|
|
218
|
-
: "
|
|
219
|
-
:
|
|
220
|
-
? `Proceed with caution โ ${findings.length} issue(s) to review.`
|
|
221
|
-
: `Not ready โ ${findings.length} issue(s): ${crit} critical, ${high} high, ${med} medium, ${low} low.`;
|
|
253
|
+
? "Automated web checks completed โ no deterministic findings in the exercised surfaces. Sampled control probes are advisory. An observation, not a release decision, and not a content, privacy, brand, or business-claim review."
|
|
254
|
+
: "No issues surfaced in the exercised surfaces. An observation, not a release decision."
|
|
255
|
+
: `${findings.length} issue(s) surfaced for review (${crit} critical, ${high} high, ${med} medium, ${low} low). An observation, not a release decision.`;
|
|
222
256
|
|
|
223
257
|
// The verdict's own honesty label: exactly which defect classes this run checked, which
|
|
224
258
|
// it structurally could NOT check, and which conditions never came up โ so "checked" is
|
|
@@ -230,7 +264,7 @@ export function buildQaReport(markersFilePath, { platform = "ios" } = {}) {
|
|
|
230
264
|
if (platform === "web") {
|
|
231
265
|
checkedFor = [
|
|
232
266
|
"page errors (uncaught exceptions)", "failed/5xx requests", "broken links (404)",
|
|
233
|
-
"placeholder links with no destination", "dead
|
|
267
|
+
"placeholder links with no destination", "sampled dead-button probes (advisory)", "error text on pages", "load timeouts",
|
|
234
268
|
];
|
|
235
269
|
notChecked = [
|
|
236
270
|
"app-specific business logic (cover with Flows: record or generate, then assert)",
|
|
@@ -271,14 +305,21 @@ export function buildQaReport(markersFilePath, { platform = "ios" } = {}) {
|
|
|
271
305
|
else conditionsNotReached.push("sign-in (no login form encountered this run)");
|
|
272
306
|
|
|
273
307
|
return {
|
|
274
|
-
verdict
|
|
275
|
-
//
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
308
|
+
// An ExplorationRun observation: findings + coverage + evidence, NO ship verdict or score
|
|
309
|
+
// (ADR-0005). The gate (evaluateGate) turns this into a pass/fail/inconclusive release outcome.
|
|
310
|
+
kind: "tapp-exploration-run",
|
|
311
|
+
schemaVersion: 1,
|
|
312
|
+
// Complete ExplorationRun contract (ADR-0005 ยง4). runStatus/stopReason describe HOW the run
|
|
313
|
+
// ended; coverage/evidence/uiMap/comparison are the structured observation. uiMap and comparison
|
|
314
|
+
// are populated by consumers that build the map / diff a baseline (null in the bare observation).
|
|
315
|
+
runStatus: inconclusive ? "limited" : "completed",
|
|
316
|
+
stopReason: inconclusive ? "coverage-floor-not-met" : "completed",
|
|
280
317
|
headline,
|
|
281
318
|
inconclusive,
|
|
319
|
+
coverage: { screensExplored, actionsPerformed, screens: Array.from(screens) },
|
|
320
|
+
evidence: { markers: base.relativeMarkersFilePath },
|
|
321
|
+
uiMap: null,
|
|
322
|
+
comparison: null,
|
|
282
323
|
checkedFor,
|
|
283
324
|
notChecked,
|
|
284
325
|
conditionsNotReached,
|
|
@@ -286,6 +327,20 @@ export function buildQaReport(markersFilePath, { platform = "ios" } = {}) {
|
|
|
286
327
|
screensExplored,
|
|
287
328
|
actionsPerformed,
|
|
288
329
|
findingCounts: { critical: crit, high, medium: med, low, total: findings.length },
|
|
330
|
+
deterministicFindingCounts: {
|
|
331
|
+
critical: verdictCrit,
|
|
332
|
+
high: verdictHigh,
|
|
333
|
+
medium: verdictMed,
|
|
334
|
+
low: verdictLow,
|
|
335
|
+
total: verdictFindings.length,
|
|
336
|
+
},
|
|
337
|
+
sampledFindingCounts: {
|
|
338
|
+
critical: sampledFindings.filter((f) => f.severity === "critical").length,
|
|
339
|
+
high: sampledFindings.filter((f) => f.severity === "high").length,
|
|
340
|
+
medium: sampledFindings.filter((f) => f.severity === "medium").length,
|
|
341
|
+
low: sampledFindings.filter((f) => f.severity === "low").length,
|
|
342
|
+
total: sampledFindings.length,
|
|
343
|
+
},
|
|
289
344
|
findings,
|
|
290
345
|
screens: Array.from(screens),
|
|
291
346
|
screenElementCounts,
|
|
@@ -314,6 +369,7 @@ export function computeContentCollapse(currentCounts, baselineCounts) {
|
|
|
314
369
|
type: "content_collapse",
|
|
315
370
|
severity: "high",
|
|
316
371
|
category: "content_collapse",
|
|
372
|
+
authority: "deterministic",
|
|
317
373
|
title: `Screen lost most of its content (${base} โ ${cur} elements)`,
|
|
318
374
|
screen,
|
|
319
375
|
step: null,
|
|
@@ -342,6 +398,7 @@ export function computeReachabilityLoss(current, baseline) {
|
|
|
342
398
|
type: "screen_unreachable",
|
|
343
399
|
severity: "high",
|
|
344
400
|
category: "navigation_dead_end",
|
|
401
|
+
authority: "deterministic",
|
|
345
402
|
title: "Screen explored in the baseline was never reached this run",
|
|
346
403
|
screen,
|
|
347
404
|
step: null,
|
|
@@ -380,15 +437,84 @@ export function computeRegression(current, baseline) {
|
|
|
380
437
|
const newFindings = current.filter((f) => !currentMatches(f));
|
|
381
438
|
const persisting = current.filter((f) => currentMatches(f));
|
|
382
439
|
const resolved = baseline.filter((b) => !baselineMatched(b));
|
|
383
|
-
const newCritical = newFindings.filter((f) => f.severity === "critical").length;
|
|
384
|
-
const newHigh = newFindings.filter((f) => f.severity === "high").length;
|
|
385
440
|
|
|
441
|
+
// Comparison ONLY โ no gate/pass/fail signal (ADR-0005). Exploration surfaces this diff; the merge
|
|
442
|
+
// decision is the gate's job. evaluateGate derives its regression fail from `newFindings` severities.
|
|
386
443
|
return {
|
|
387
444
|
hadBaseline: true,
|
|
388
445
|
counts: { new: newFindings.length, persisting: persisting.length, resolved: resolved.length },
|
|
389
446
|
newFindings,
|
|
390
447
|
resolved,
|
|
391
|
-
|
|
392
|
-
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// The gate's public outcome model (ADR-0005). A merge gate is ultimately block / don't-block, but
|
|
452
|
+
// callers need to distinguish WHY: a deterministic violation is not the same as "we couldn't get
|
|
453
|
+
// the evidence." Exit codes are the CI contract; precedence is fail > inconclusive > pass.
|
|
454
|
+
export const GATE_EXIT = { pass: 0, fail: 1, error: 2, inconclusive: 3 };
|
|
455
|
+
// Bump when the gate's decision semantics change (NOT the npm version). Recorded on every GateRun.
|
|
456
|
+
export const GATE_POLICY_VERSION = "1";
|
|
457
|
+
|
|
458
|
+
// Pure gate evaluator: frozen evidence + policy โ a GateRun decision. Extracted verbatim from the
|
|
459
|
+
// former inline logic in ci-report.js so the `[char]` characterization tests keep passing โ the
|
|
460
|
+
// merge decision (block/don't-block) is unchanged; this only classifies each reason as a
|
|
461
|
+
// deterministic `fail` or an evidence-absent `inconclusive` and folds them by precedence. Reason
|
|
462
|
+
// MESSAGES are preserved exactly (several are asserted by tests).
|
|
463
|
+
//
|
|
464
|
+
// DECOUPLED FROM THE SCORE (ADR-0005): the block-by-findings decision reads `deterministicFindingCounts` +
|
|
465
|
+
// `inconclusive` via `findingsBlock`, NOT the score scalar or the `verdict` label. `verdict`/
|
|
466
|
+
// `releaseScore` can therefore be removed from exploration output without changing any merge
|
|
467
|
+
// decision. The `riskScore < 50` threshold is retained deliberately (kept explicit, inside
|
|
468
|
+
// `findingsBlock`) and locked by the `[char]` risk-threshold test.
|
|
469
|
+
export function evaluateGate({ report, regression = null, flows = [], scenarios = [], contracts = [], prPlan = null, baseline = null, failOn = "gate" } = {}) {
|
|
470
|
+
const reasons = []; // { kind: "fail" | "inconclusive", message }
|
|
471
|
+
const fail = (message) => reasons.push({ kind: "fail", message });
|
|
472
|
+
const inconclusive = (message) => reasons.push({ kind: "inconclusive", message });
|
|
473
|
+
|
|
474
|
+
// Classify each replayed suite by evidence authority (ADR-0005): a DETERMINISTIC step failure (or
|
|
475
|
+
// a non-model failure like an aborted/missing run) is a real fail; a suite with no deterministic
|
|
476
|
+
// failure that carries a model-observed (assert_ai) assertion cannot be decided by the default
|
|
477
|
+
// deterministic gate โ inconclusive/needs-review (it must not silently pass, and a model verdict
|
|
478
|
+
// must not masquerade as a deterministic fail). No `--policy probabilistic` opt-in in 0.17.
|
|
479
|
+
const deterministicFail = (s) => s.deterministicFailed === true || (s.passed === false && !s.modelObserved);
|
|
480
|
+
const classify = (s) => (deterministicFail(s) ? "fail" : s.modelObserved ? "needs-review" : "pass");
|
|
481
|
+
for (const [label, suites] of [["flow", flows], ["multi-actor scenario", scenarios], ["release contract", contracts]]) {
|
|
482
|
+
const failed = suites.filter((s) => classify(s) === "fail");
|
|
483
|
+
if (failed.length) fail(`${failed.length} ${label}(s) failed`);
|
|
484
|
+
const needsReview = suites.filter((s) => classify(s) === "needs-review");
|
|
485
|
+
if (needsReview.length) inconclusive(`${needsReview.length} ${label}(s) contain assert_ai (model-observed); the deterministic gate cannot decide them โ review, or add an explicit probabilistic policy`);
|
|
486
|
+
}
|
|
487
|
+
// Selected-but-unexecuted work is missing evidence, not an observed violation โ inconclusive.
|
|
488
|
+
if (prPlan?.execution?.notRun) inconclusive(`${prPlan.execution.notRun} selected release contract(s) did not run`);
|
|
489
|
+
if (prPlan?.execution?.explorationFailed) inconclusive(`${prPlan.execution.explorationFailed} planned PR exploration target(s) failed or were not reached`);
|
|
490
|
+
|
|
491
|
+
if (failOn === "any") {
|
|
492
|
+
if (report.findingCounts.total > 0) fail(`${report.findingCounts.total} finding(s) (fail-on: any)`);
|
|
493
|
+
// "any" is the strictest policy โ an inconclusive run (evidence not obtained) must never pass it.
|
|
494
|
+
if (report.inconclusive) inconclusive("run was inconclusive (coverage floor not met)");
|
|
495
|
+
} else if (failOn === "absolute" || (failOn === "gate" && !regression)) {
|
|
496
|
+
if (findingsBlock(report.deterministicFindingCounts, { inconclusive: report.inconclusive })) fail("blocking deterministic finding(s)");
|
|
497
|
+
if (report.inconclusive) inconclusive("run was inconclusive (coverage floor not met)");
|
|
498
|
+
} else {
|
|
499
|
+
if (regression?.newFindings?.length) {
|
|
500
|
+
const newCritical = regression.newFindings.filter((f) => f.severity === "critical").length;
|
|
501
|
+
const newHigh = regression.newFindings.filter((f) => f.severity === "high").length;
|
|
502
|
+
if (newCritical + newHigh > 0) fail(`${newCritical} new critical + ${newHigh} new high vs. baseline`);
|
|
503
|
+
}
|
|
504
|
+
if (findingsBlock(report.deterministicFindingCounts, { inconclusive: report.inconclusive })) fail("blocking deterministic finding(s)");
|
|
505
|
+
if (report.inconclusive && !baseline?.inconclusive) inconclusive("run became inconclusive vs. baseline (app may no longer launch/explore)");
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
const outcome = reasons.some((r) => r.kind === "fail") ? "fail"
|
|
509
|
+
: reasons.some((r) => r.kind === "inconclusive") ? "inconclusive"
|
|
510
|
+
: "pass";
|
|
511
|
+
return {
|
|
512
|
+
policy: failOn,
|
|
513
|
+
outcome,
|
|
514
|
+
exitCode: GATE_EXIT[outcome],
|
|
515
|
+
failed: outcome !== "pass", // retained for markdown/JSON consumers during migration
|
|
516
|
+
reasons: reasons.map((r) => r.message),
|
|
517
|
+
reasonDetails: reasons,
|
|
518
|
+
policyVersion: GATE_POLICY_VERSION,
|
|
393
519
|
};
|
|
394
520
|
}
|
|
@@ -109,7 +109,7 @@ export function loadTaskRegistry({ sourcePath, projectDir = "", taskFiles = [] }
|
|
|
109
109
|
// Draft contracts generated under `.tapp/proposals/contracts` may compile
|
|
110
110
|
// against sibling untrusted Task drafts. Ordinary committed contracts never
|
|
111
111
|
// see this directory, so a proposal cannot silently enter the release gate.
|
|
112
|
-
const proposalSource =
|
|
112
|
+
const proposalSource = String(path.resolve(sourcePath || "")).includes(`${path.sep}.tapp${path.sep}proposals${path.sep}`);
|
|
113
113
|
const proposalDir = proposalSource && tappDir ? path.join(tappDir, "proposals", "tasks") : "";
|
|
114
114
|
const proposed = proposalDir && fs.existsSync(proposalDir)
|
|
115
115
|
? fs.readdirSync(proposalDir).filter((name) => /\.ya?ml$|\.json$/i.test(name)).map((name) => path.join(proposalDir, name))
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aarwitz/tapp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.17.0-rc.1",
|
|
4
4
|
"mcpName": "io.github.aarwitz/tapp",
|
|
5
5
|
"description": "Release contracts, autonomous QA, and evidence-backed CI gates for iOS, Android, and web.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -87,7 +87,7 @@
|
|
|
87
87
|
"mobile"
|
|
88
88
|
],
|
|
89
89
|
"scripts": {
|
|
90
|
-
"test": "node --test tests/report.test.js tests/regression.test.js tests/engine.test.js tests/project-config.test.js tests/application-model.test.js tests/ui-map.test.js tests/task-runtime.test.js tests/release-contract.test.js tests/pr-selection.test.js tests/flow-runtime.test.js tests/web-explorer.test.js tests/web-session.test.js tests/web-flow.test.js tests/scenario-runtime.test.js tests/android-driver.test.js tests/android-explorer.test.js tests/android-flow.test.js tests/managed-web.test.js tests/product-operations.test.js tests/browser-product.test.js tests/browser-onboarding.test.js tests/managed-operation.test.js tests/ci-setup.test.js tests/ci-install.test.js tests/cli.test.js tests/action.test.js tests/package-surface.test.js tests/ci-gate.test.js tests/ci-report.test.js tests/desktop-protocol.test.js tests/ios-flow-protocol.test.js",
|
|
90
|
+
"test": "node --test tests/report.test.js tests/regression.test.js tests/engine.test.js tests/project-config.test.js tests/application-model.test.js tests/ui-map.test.js tests/task-runtime.test.js tests/release-contract.test.js tests/pr-selection.test.js tests/flow-runtime.test.js tests/web-explorer.test.js tests/web-session.test.js tests/web-flow.test.js tests/scenario-runtime.test.js tests/android-driver.test.js tests/android-explorer.test.js tests/android-flow.test.js tests/managed-web.test.js tests/product-operations.test.js tests/browser-product.test.js tests/browser-onboarding.test.js tests/managed-operation.test.js tests/cloud-runner.test.js tests/ci-setup.test.js tests/ci-install.test.js tests/cli.test.js tests/action.test.js tests/package-surface.test.js tests/ci-gate.test.js tests/ci-report.test.js tests/desktop-protocol.test.js tests/ios-flow-protocol.test.js",
|
|
91
91
|
"test:browser-journey": "node --test tests/browser-journey.test.js",
|
|
92
92
|
"test:browser-native": "TAPP_RUN_NATIVE_BROWSER=1 node --test tests/browser-native-journey.test.js"
|
|
93
93
|
}
|
package/scripts/ci-gate.sh
CHANGED
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
# [--pr-plan-out <file.json>] # persist the reviewable selection plan
|
|
26
26
|
# [--baseline <file.json>] # prior report to diff against (skipped if absent)
|
|
27
27
|
# [--target-key <stable-id>] # isolates target-specific baselines in monorepos
|
|
28
|
-
# [--fail-on gate|
|
|
28
|
+
# [--fail-on gate|absolute|any] # gate policy (default gate; see ci-report.js)
|
|
29
29
|
# [--json-out <file.json>] # write the full report (use as the next baseline)
|
|
30
30
|
# [--md-out <file.md>] # write the rendered markdown report (for a PR comment)
|
|
31
31
|
# [--device <name>] # simulator device to boot if none is (default "iPhone 16 Pro")
|
|
@@ -74,7 +74,7 @@ done
|
|
|
74
74
|
[[ "$PLATFORM" == "ios" || "$PLATFORM" == "android" || "$PLATFORM" == "web" ]] || { echo "โ --platform must be ios|android|web" >&2; exit 2; }
|
|
75
75
|
[[ "$ACTIONS" =~ ^[1-9][0-9]*$ ]] || { echo "โ --actions must be a positive integer" >&2; exit 2; }
|
|
76
76
|
[[ "$TIMEOUT" =~ ^[1-9][0-9]*$ ]] || { echo "โ --timeout must be a positive integer" >&2; exit 2; }
|
|
77
|
-
[[ "$FAIL_ON" == "gate" || "$FAIL_ON" == "
|
|
77
|
+
[[ "$FAIL_ON" == "gate" || "$FAIL_ON" == "absolute" || "$FAIL_ON" == "any" ]] || { echo "โ --fail-on must be gate|absolute|any" >&2; exit 2; }
|
|
78
78
|
if [[ -n "$PROJECT_DIR" ]]; then
|
|
79
79
|
[[ -d "$PROJECT_DIR" ]] || { echo "โ Project directory not found: $PROJECT_DIR" >&2; exit 2; }
|
|
80
80
|
PROJECT_DIR="$(cd "$PROJECT_DIR" && pwd)"
|
|
@@ -274,7 +274,7 @@ xcrun simctl install "$UDID" "$APP_PATH" || { echo "โ simctl install failed
|
|
|
274
274
|
# โโ Autonomous exploration (quick-capture builds the harness itself if needed).
|
|
275
275
|
step "Explore ($ACTIONS actions, ${TIMEOUT}s watchdog)"
|
|
276
276
|
set +e
|
|
277
|
-
CAPTURE_ROOT="${TAPP_HOME:-$
|
|
277
|
+
CAPTURE_ROOT="${TAPP_HOME:-$ROOT}/captures"
|
|
278
278
|
mkdir -p "$CAPTURE_ROOT"
|
|
279
279
|
CAPTURE_DIR="$(mktemp -d "$CAPTURE_ROOT/ci.XXXXXX")"
|
|
280
280
|
TAPP_CAPTURE_DIR="$CAPTURE_DIR" OCQA_PR_TARGET_JSON="$IOS_PR_TARGET_JSON" "$ROOT/scripts/quick-capture.sh" explore "$BUNDLE_ID" --actions "$ACTIONS" --timeout "$TIMEOUT"
|
package/scripts/quick-capture.sh
CHANGED
|
@@ -19,7 +19,7 @@ PROJECT_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
|
|
19
19
|
# accidentally reuse stale markers). TAPP_HOME redirects all writable output otherwise โ
|
|
20
20
|
# set by the `tapp` CLI when running as an installed npm package, where the package dir
|
|
21
21
|
# must stay read-only. Unset (repo dev flow), everything lands in the repo as before.
|
|
22
|
-
TAPP_RUNTIME_HOME="${TAPP_HOME
|
|
22
|
+
TAPP_RUNTIME_HOME="${TAPP_HOME:-}"
|
|
23
23
|
if [[ -n "${TAPP_CAPTURE_DIR:-}" ]]; then
|
|
24
24
|
CAPTURE_DIR="$TAPP_CAPTURE_DIR"
|
|
25
25
|
elif [[ -n "$TAPP_RUNTIME_HOME" ]]; then
|
package/scripts/run-flow.sh
CHANGED
|
@@ -26,7 +26,7 @@ UDID="$(xcrun simctl list devices booted -j 2>/dev/null | python3 -c 'import sys
|
|
|
26
26
|
[ -z "$UDID" ] && { echo "โ No booted simulator."; exit 2; }
|
|
27
27
|
# When run through the installed `tapp` CLI, the harness cache lives under TAPP_HOME.
|
|
28
28
|
XCTR=""
|
|
29
|
-
TAPP_RUNTIME_HOME="${TAPP_HOME
|
|
29
|
+
TAPP_RUNTIME_HOME="${TAPP_HOME:-}"
|
|
30
30
|
[ -n "$TAPP_RUNTIME_HOME" ] && XCTR="$(find "$TAPP_RUNTIME_HOME/harness-derived/Build/Products" -name '*.xctestrun' 2>/dev/null | head -1)"
|
|
31
31
|
[ -z "$XCTR" ] && XCTR="$(find "$HOME/Library/Developer/Xcode/DerivedData/OCQAHarness-"*/Build/Products -name '*.xctestrun' 2>/dev/null | head -1)"
|
|
32
32
|
[ -z "$XCTR" ] && XCTR="$(find /tmp/tapp-harness-derived/Build/Products -name '*.xctestrun' 2>/dev/null | head -1)"
|