@aarwitz/tapp 0.16.5 โ 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 -21
- package/Harness/OCQAHarnessUITests/ExplorerTests.swift +1 -1
- package/README.md +64 -67
- 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 +82 -60
- 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 -4
- package/mcp-server/src/index.js +140 -46
- 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 +123 -38
- 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
|
@@ -106,16 +106,31 @@ export function findingEvaluationTier(finding, platform = "ios") {
|
|
|
106
106
|
return platform === "web" && WEB_SAMPLED_ISSUE_TYPES.has(finding?.type) ? "sampled" : "deterministic";
|
|
107
107
|
}
|
|
108
108
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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";
|
|
112
129
|
}
|
|
113
130
|
|
|
114
|
-
export function
|
|
115
|
-
const
|
|
116
|
-
|
|
117
|
-
if (report?.platform === "web") return "exploratory web ยท no scalar score";
|
|
118
|
-
return "score unavailable";
|
|
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`;
|
|
119
134
|
}
|
|
120
135
|
|
|
121
136
|
// Turn a capture's OCQA markers into the same ship/no-ship report Tapp produces:
|
|
@@ -201,6 +216,10 @@ export function buildQaReport(markersFilePath, { platform = "ios" } = {}) {
|
|
|
201
216
|
findings.push({
|
|
202
217
|
...i,
|
|
203
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",
|
|
204
223
|
...(platform === "web" ? { evaluationTier: findingEvaluationTier(i, platform) } : {}),
|
|
205
224
|
});
|
|
206
225
|
}
|
|
@@ -222,27 +241,18 @@ export function buildQaReport(markersFilePath, { platform = "ios" } = {}) {
|
|
|
222
241
|
const verdictLow = verdictFindings.filter((f) => f.severity === "low").length;
|
|
223
242
|
const sampledFindings = platform === "web" ? findings.filter((finding) => finding.evaluationTier === "sampled") : [];
|
|
224
243
|
|
|
225
|
-
// 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.
|
|
226
247
|
const inconclusive = screensExplored < 2 || actionsPerformed < 3;
|
|
227
|
-
let riskScore = Math.max(0, Math.min(100, 100 - verdictCrit * 25 - verdictHigh * 10 - verdictMed * 3));
|
|
228
|
-
if (inconclusive) riskScore = Math.min(riskScore, 40);
|
|
229
|
-
|
|
230
|
-
let verdict;
|
|
231
|
-
if (verdictCrit > 0) verdict = "blocked";
|
|
232
|
-
else if (inconclusive) verdict = "caution";
|
|
233
|
-
else if (riskScore < 50) verdict = "blocked";
|
|
234
|
-
else if (verdictHigh > 0 || riskScore < 80) verdict = "caution";
|
|
235
|
-
else verdict = "ready";
|
|
236
248
|
|
|
237
249
|
const headline = inconclusive
|
|
238
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.`
|
|
239
|
-
:
|
|
251
|
+
: findings.length === 0
|
|
240
252
|
? platform === "web"
|
|
241
|
-
? "Automated web checks completed โ no
|
|
242
|
-
: "
|
|
243
|
-
:
|
|
244
|
-
? `Proceed with caution โ ${findings.length} issue(s) to review.`
|
|
245
|
-
: `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.`;
|
|
246
256
|
|
|
247
257
|
// The verdict's own honesty label: exactly which defect classes this run checked, which
|
|
248
258
|
// it structurally could NOT check, and which conditions never came up โ so "checked" is
|
|
@@ -295,17 +305,21 @@ export function buildQaReport(markersFilePath, { platform = "ios" } = {}) {
|
|
|
295
305
|
else conditionsNotReached.push("sign-in (no login form encountered this run)");
|
|
296
306
|
|
|
297
307
|
return {
|
|
298
|
-
verdict
|
|
299
|
-
//
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
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",
|
|
307
317
|
headline,
|
|
308
318
|
inconclusive,
|
|
319
|
+
coverage: { screensExplored, actionsPerformed, screens: Array.from(screens) },
|
|
320
|
+
evidence: { markers: base.relativeMarkersFilePath },
|
|
321
|
+
uiMap: null,
|
|
322
|
+
comparison: null,
|
|
309
323
|
checkedFor,
|
|
310
324
|
notChecked,
|
|
311
325
|
conditionsNotReached,
|
|
@@ -313,7 +327,7 @@ export function buildQaReport(markersFilePath, { platform = "ios" } = {}) {
|
|
|
313
327
|
screensExplored,
|
|
314
328
|
actionsPerformed,
|
|
315
329
|
findingCounts: { critical: crit, high, medium: med, low, total: findings.length },
|
|
316
|
-
|
|
330
|
+
deterministicFindingCounts: {
|
|
317
331
|
critical: verdictCrit,
|
|
318
332
|
high: verdictHigh,
|
|
319
333
|
medium: verdictMed,
|
|
@@ -355,6 +369,7 @@ export function computeContentCollapse(currentCounts, baselineCounts) {
|
|
|
355
369
|
type: "content_collapse",
|
|
356
370
|
severity: "high",
|
|
357
371
|
category: "content_collapse",
|
|
372
|
+
authority: "deterministic",
|
|
358
373
|
title: `Screen lost most of its content (${base} โ ${cur} elements)`,
|
|
359
374
|
screen,
|
|
360
375
|
step: null,
|
|
@@ -383,6 +398,7 @@ export function computeReachabilityLoss(current, baseline) {
|
|
|
383
398
|
type: "screen_unreachable",
|
|
384
399
|
severity: "high",
|
|
385
400
|
category: "navigation_dead_end",
|
|
401
|
+
authority: "deterministic",
|
|
386
402
|
title: "Screen explored in the baseline was never reached this run",
|
|
387
403
|
screen,
|
|
388
404
|
step: null,
|
|
@@ -421,15 +437,84 @@ export function computeRegression(current, baseline) {
|
|
|
421
437
|
const newFindings = current.filter((f) => !currentMatches(f));
|
|
422
438
|
const persisting = current.filter((f) => currentMatches(f));
|
|
423
439
|
const resolved = baseline.filter((b) => !baselineMatched(b));
|
|
424
|
-
const newCritical = newFindings.filter((f) => f.severity === "critical").length;
|
|
425
|
-
const newHigh = newFindings.filter((f) => f.severity === "high").length;
|
|
426
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.
|
|
427
443
|
return {
|
|
428
444
|
hadBaseline: true,
|
|
429
445
|
counts: { new: newFindings.length, persisting: persisting.length, resolved: resolved.length },
|
|
430
446
|
newFindings,
|
|
431
447
|
resolved,
|
|
432
|
-
|
|
433
|
-
|
|
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,
|
|
434
519
|
};
|
|
435
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)"
|