@aarwitz/tapp 0.15.0 → 0.16.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/AGENTS.md +4 -4
- package/Harness/OCQAHarness/AppDelegate.swift +1 -1
- package/Harness/OCQAHarness.xcodeproj/project.pbxproj +2 -2
- package/Harness/OCQAHarnessUITests/ExplorerTests.swift +2 -2
- package/Harness/generate-harness-xcodeproj.rb +3 -3
- package/README.md +17 -15
- package/bin/tapp.js +28 -28
- package/browser/app.js +2 -2
- package/browser/index.html +1 -1
- package/docs/BROWSER-PRODUCT.md +2 -2
- package/docs/PRODUCT-ENGINE.md +7 -6
- package/docs/application-model.md +20 -25
- package/docs/scenarios.md +7 -7
- package/mcp-server/src/application-model.js +32 -26
- package/mcp-server/src/browser-product.js +1 -1
- package/mcp-server/src/ci-report.js +5 -4
- package/mcp-server/src/ci-setup.js +16 -7
- package/mcp-server/src/enrich.js +1 -1
- package/mcp-server/src/index.js +63 -62
- package/mcp-server/src/maintenance-proposal.js +4 -4
- package/mcp-server/src/pr-selection.js +9 -8
- package/mcp-server/src/product-execution.js +1 -1
- package/mcp-server/src/product-operations.js +20 -18
- package/mcp-server/src/project-config.js +8 -5
- package/mcp-server/src/project-paths.js +32 -0
- package/mcp-server/src/report.js +3 -3
- package/mcp-server/src/task-runtime.js +14 -10
- package/package.json +15 -3
- package/scripts/ci-gate.sh +15 -10
- package/scripts/flow_ai_judge.py +1 -1
- package/scripts/flow_lib.py +1 -1
- package/scripts/quick-capture.sh +15 -14
- package/scripts/run-flow.sh +6 -6
- package/scripts/android-corpus-e2e.sh +0 -30
- package/scripts/cleanup-xcode.sh +0 -157
- package/scripts/corpus-apps.txt +0 -9
- package/scripts/corpus-sweep.sh +0 -121
- package/scripts/coverage-eval.sh +0 -92
- package/scripts/coverage_eval_parse.py +0 -95
- package/scripts/deploy-and-build.sh +0 -99
- package/scripts/mutation-recall-desktop.sh +0 -186
- package/scripts/mutation-recall.sh +0 -121
- package/scripts/mutation_lib.py +0 -128
- package/scripts/mutation_operators.py +0 -144
- package/scripts/validation-matrix.sh +0 -146
- package/scripts/vision-fp-eval.sh +0 -206
- package/scripts/vision_escalation_responder.py +0 -147
- package/scripts/vision_fp_probe.py +0 -221
|
@@ -16,8 +16,9 @@ import {
|
|
|
16
16
|
reviewReleasePlan,
|
|
17
17
|
writeInitArtifacts,
|
|
18
18
|
} from "./application-model.js";
|
|
19
|
-
import { baselinePathForTarget, renderGithubWorkflow, selectApplicationTarget, writeCiInstallation, writeTargetBaseline } from "./ci-setup.js";
|
|
19
|
+
import { baselinePathForTarget, existingBaselinePathForTarget, renderGithubWorkflow, selectApplicationTarget, writeCiInstallation, writeTargetBaseline } from "./ci-setup.js";
|
|
20
20
|
import { executeReleaseContract, runProductProcess } from "./product-execution.js";
|
|
21
|
+
import { projectArtifactDirectory } from "./project-paths.js";
|
|
21
22
|
|
|
22
23
|
const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
|
|
23
24
|
const packageVersion = JSON.parse(fs.readFileSync(path.join(packageRoot, "package.json"), "utf8")).version;
|
|
@@ -50,8 +51,9 @@ function atomicJson(file, value) {
|
|
|
50
51
|
fs.renameSync(temporary, file);
|
|
51
52
|
}
|
|
52
53
|
|
|
53
|
-
function artifactPaths(root, outDir = ".
|
|
54
|
-
const
|
|
54
|
+
function artifactPaths(root, outDir = ".tapp") {
|
|
55
|
+
const selectedOutDir = projectArtifactDirectory(root, outDir);
|
|
56
|
+
const dir = path.resolve(root, selectedOutDir);
|
|
55
57
|
if (!inside(root, dir)) throw new Error("Artifact directory must remain inside the repository");
|
|
56
58
|
return {
|
|
57
59
|
dir,
|
|
@@ -63,7 +65,7 @@ function artifactPaths(root, outDir = ".autotap") {
|
|
|
63
65
|
}
|
|
64
66
|
|
|
65
67
|
function productRunRoot(root) {
|
|
66
|
-
const home = process.env.
|
|
68
|
+
const home = process.env.TAPP_HOME || process.env.AUTOTAP_HOME || path.join(os.homedir(), ".tapp");
|
|
67
69
|
const identity = crypto.createHash("sha256").update(root).digest("hex").slice(0, 16);
|
|
68
70
|
return path.join(home, "product-runs", identity);
|
|
69
71
|
}
|
|
@@ -94,7 +96,7 @@ function listProductRuns(root) {
|
|
|
94
96
|
}
|
|
95
97
|
|
|
96
98
|
function listRepositoryFlows(root) {
|
|
97
|
-
const directory = path.join(root,
|
|
99
|
+
const directory = path.join(root, projectArtifactDirectory(root), "flows");
|
|
98
100
|
if (!fs.existsSync(directory)) return [];
|
|
99
101
|
return fs.readdirSync(directory, { withFileTypes:true })
|
|
100
102
|
.filter((entry) => entry.isFile() && /\.(?:ya?ml|json)$/i.test(entry.name))
|
|
@@ -127,7 +129,7 @@ function listRepositoryFlows(root) {
|
|
|
127
129
|
.sort((left, right) => left.path.localeCompare(right.path));
|
|
128
130
|
}
|
|
129
131
|
|
|
130
|
-
export function readProductProject({ projectDir, outDir = ".
|
|
132
|
+
export function readProductProject({ projectDir, outDir = ".tapp" } = {}) {
|
|
131
133
|
const root = realProject(projectDir);
|
|
132
134
|
const paths = artifactPaths(root, outDir);
|
|
133
135
|
const model = readJson(paths.model);
|
|
@@ -137,7 +139,7 @@ export function readProductProject({ projectDir, outDir = ".autotap" } = {}) {
|
|
|
137
139
|
const requirements = model?.requirements || [];
|
|
138
140
|
const planItems = plan?.items || [];
|
|
139
141
|
const baselines = (model?.targets || []).map((target) => {
|
|
140
|
-
const file =
|
|
142
|
+
const file = existingBaselinePathForTarget(root, target);
|
|
141
143
|
return { targetId: target.id, platform: target.platform, path: file, relativePath: path.relative(root, file).replaceAll(path.sep, "/"), exists: fs.existsSync(file) };
|
|
142
144
|
});
|
|
143
145
|
return {
|
|
@@ -177,7 +179,7 @@ export function readProductProject({ projectDir, outDir = ".autotap" } = {}) {
|
|
|
177
179
|
// target identity or configuration rules.
|
|
178
180
|
export async function prepareProductTarget({
|
|
179
181
|
projectDir,
|
|
180
|
-
outDir = ".
|
|
182
|
+
outDir = ".tapp",
|
|
181
183
|
platform = "",
|
|
182
184
|
target = "",
|
|
183
185
|
appPath = "",
|
|
@@ -262,7 +264,7 @@ export async function prepareProductTarget({
|
|
|
262
264
|
export async function initializeProductProject({
|
|
263
265
|
projectDir,
|
|
264
266
|
mode = "inspect",
|
|
265
|
-
outDir = ".
|
|
267
|
+
outDir = ".tapp",
|
|
266
268
|
ownedUrl = "",
|
|
267
269
|
platform = "",
|
|
268
270
|
target = "",
|
|
@@ -326,7 +328,7 @@ function resolvePlan(root, outDir, planPath = "") {
|
|
|
326
328
|
return { file, plan: readJson(file, { required: true }) };
|
|
327
329
|
}
|
|
328
330
|
|
|
329
|
-
export function reviewProductPlan({ projectDir, outDir = ".
|
|
331
|
+
export function reviewProductPlan({ projectDir, outDir = ".tapp", planPath = "", approve = [], reject = [], defer = [] } = {}) {
|
|
330
332
|
const root = realProject(projectDir);
|
|
331
333
|
const resolved = resolvePlan(root, outDir, planPath);
|
|
332
334
|
const plan = reviewReleasePlan(resolved.plan, { approve, reject, defer });
|
|
@@ -334,7 +336,7 @@ export function reviewProductPlan({ projectDir, outDir = ".autotap", planPath =
|
|
|
334
336
|
return { operation: "review-plan", plan, planPath: resolved.file, project: readProductProject({ projectDir: root, outDir }) };
|
|
335
337
|
}
|
|
336
338
|
|
|
337
|
-
export async function generateProductPlan({ projectDir, outDir = ".
|
|
339
|
+
export async function generateProductPlan({ projectDir, outDir = ".tapp", planPath = "" } = {}) {
|
|
338
340
|
const root = realProject(projectDir);
|
|
339
341
|
const resolved = resolvePlan(root, outDir, planPath);
|
|
340
342
|
const result = await generateApprovedContractProposals(resolved.plan, { projectDir: root });
|
|
@@ -344,7 +346,7 @@ export async function generateProductPlan({ projectDir, outDir = ".autotap", pla
|
|
|
344
346
|
|
|
345
347
|
export async function validateProductPlan({
|
|
346
348
|
projectDir,
|
|
347
|
-
outDir = ".
|
|
349
|
+
outDir = ".tapp",
|
|
348
350
|
planPath = "",
|
|
349
351
|
items = [],
|
|
350
352
|
platform = "",
|
|
@@ -411,7 +413,7 @@ export async function validateProductPlan({
|
|
|
411
413
|
return { operation: "validate-plan", platform: selectedPlatform, passed: results.every((item) => item.passed), results, plan, planPath: resolved.file, project: readProductProject({ projectDir: root, outDir }) };
|
|
412
414
|
}
|
|
413
415
|
|
|
414
|
-
export async function promoteProductPlan({ projectDir, outDir = ".
|
|
416
|
+
export async function promoteProductPlan({ projectDir, outDir = ".tapp", planPath = "", items = [] } = {}) {
|
|
415
417
|
const root = realProject(projectDir);
|
|
416
418
|
const resolved = resolvePlan(root, outDir, planPath);
|
|
417
419
|
const result = await promoteValidatedProposals(resolved.plan, { projectDir: root, ids: items || [] });
|
|
@@ -423,7 +425,7 @@ export async function promoteProductPlan({ projectDir, outDir = ".autotap", plan
|
|
|
423
425
|
return { operation: "promote-plan", ...result, plan: written.plan, planPath: written.planPath, modelPath: written.modelPath, project: readProductProject({ projectDir: root, outDir }) };
|
|
424
426
|
}
|
|
425
427
|
|
|
426
|
-
export function prepareProductCi({ projectDir, outDir = ".
|
|
428
|
+
export function prepareProductCi({ projectDir, outDir = ".tapp", modelPath = "", actionRef = DEFAULT_ACTION_REF, defaultBranch = "main" } = {}) {
|
|
427
429
|
const root = realProject(projectDir);
|
|
428
430
|
const modelFile = modelPath ? path.resolve(root, modelPath) : artifactPaths(root, outDir).model;
|
|
429
431
|
if (!inside(root, modelFile)) throw new Error("Application model must remain inside the repository");
|
|
@@ -433,7 +435,7 @@ export function prepareProductCi({ projectDir, outDir = ".autotap", modelPath =
|
|
|
433
435
|
return { operation: "prepare-ci", ...rendered, project: readProductProject({ projectDir: root, outDir }) };
|
|
434
436
|
}
|
|
435
437
|
|
|
436
|
-
export function installProductCi({ projectDir, outDir = ".
|
|
438
|
+
export function installProductCi({ projectDir, outDir = ".tapp", modelPath = "", actionRef = DEFAULT_ACTION_REF, defaultBranch = "main", workflowPath = ".github/workflows/tapp.yml", manifestPath = ".tapp/ci.json", replace = false, allowUnresolved = false } = {}) {
|
|
437
439
|
const root = realProject(projectDir);
|
|
438
440
|
const rendered = prepareProductCi({ projectDir: root, outDir, modelPath, actionRef, defaultBranch });
|
|
439
441
|
if (rendered.manifest.unresolved.length && !allowUnresolved) throw new Error(`CI installation is unresolved: ${rendered.manifest.unresolved.map((item) => `${item.platform}:${item.message}`).join("; ")}`);
|
|
@@ -443,7 +445,7 @@ export function installProductCi({ projectDir, outDir = ".autotap", modelPath =
|
|
|
443
445
|
|
|
444
446
|
export async function runProductGate({
|
|
445
447
|
projectDir,
|
|
446
|
-
outDir = ".
|
|
448
|
+
outDir = ".tapp",
|
|
447
449
|
platform = "web",
|
|
448
450
|
target = "",
|
|
449
451
|
url = "",
|
|
@@ -501,7 +503,7 @@ export async function runProductGate({
|
|
|
501
503
|
if (!inside(root, baselinePath) || !fs.existsSync(baselinePath)) throw new Error("Baseline must be an existing file inside the repository");
|
|
502
504
|
args.push("--baseline", baselinePath);
|
|
503
505
|
} else {
|
|
504
|
-
const targetBaseline =
|
|
506
|
+
const targetBaseline = existingBaselinePathForTarget(root, selected);
|
|
505
507
|
if (fs.existsSync(targetBaseline)) args.push("--baseline", targetBaseline);
|
|
506
508
|
}
|
|
507
509
|
onProgress({ phase: "gate", text: `Running ${selected.platform}:${selected.name} release gate` });
|
|
@@ -515,7 +517,7 @@ export async function runProductGate({
|
|
|
515
517
|
return { operation: "run-gate", passed: execution.code === 0, code: execution.code, stdout: execution.stdout, stderr: execution.stderr, selectedTarget: selected, report, reportPath, markdownPath, runDir, project: readProductProject({ projectDir: root, outDir }) };
|
|
516
518
|
}
|
|
517
519
|
|
|
518
|
-
export function createProductBaseline({ projectDir, outDir = ".
|
|
520
|
+
export function createProductBaseline({ projectDir, outDir = ".tapp", reportPath, platform = "web", target = "", replace = false, baselinePath = "" } = {}) {
|
|
519
521
|
const root = realProject(projectDir);
|
|
520
522
|
const project = readProductProject({ projectDir: root, outDir });
|
|
521
523
|
const selected = selectApplicationTarget(project.model, { platform, target });
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import { LEGACY_TAPP_DIRECTORY, TAPP_DIRECTORY, projectArtifactDirectory } from "./project-paths.js";
|
|
3
4
|
|
|
4
|
-
export const PROJECT_CONFIG_RELATIVE_PATH =
|
|
5
|
+
export const PROJECT_CONFIG_RELATIVE_PATH = `${TAPP_DIRECTORY}/project.json`;
|
|
6
|
+
export const LEGACY_PROJECT_CONFIG_RELATIVE_PATH = `${LEGACY_TAPP_DIRECTORY}/project.json`;
|
|
5
7
|
|
|
6
8
|
const ACTOR_NAME = /^[A-Za-z][A-Za-z0-9_-]{0,63}$/;
|
|
7
9
|
const ENV_NAME = /^[A-Z_][A-Z0-9_]{0,127}$/;
|
|
@@ -58,15 +60,16 @@ export function validateProjectConfig(config) {
|
|
|
58
60
|
|
|
59
61
|
export function readProjectConfig(projectDir, { required = false } = {}) {
|
|
60
62
|
const root = fs.realpathSync(path.resolve(projectDir || process.cwd()));
|
|
61
|
-
const
|
|
63
|
+
const relativePath = `${projectArtifactDirectory(root)}/project.json`;
|
|
64
|
+
const configPath = path.join(root, relativePath);
|
|
62
65
|
if (!fs.existsSync(configPath)) {
|
|
63
66
|
if (required) throw new Error(`Project configuration not found: ${configPath}`);
|
|
64
|
-
return { root, path: configPath, relativePath
|
|
67
|
+
return { root, path: configPath, relativePath, config: { kind: "tapp-project-config", schemaVersion: 1, actors: {} }, exists: false, errors: [] };
|
|
65
68
|
}
|
|
66
69
|
let config;
|
|
67
70
|
try { config = JSON.parse(fs.readFileSync(configPath, "utf8")); }
|
|
68
|
-
catch (error) { return { root, path: configPath, relativePath
|
|
69
|
-
return { root, path: configPath, relativePath
|
|
71
|
+
catch (error) { return { root, path: configPath, relativePath, config: null, exists: true, errors: [`Invalid JSON: ${error.message}`] }; }
|
|
72
|
+
return { root, path: configPath, relativePath, config, exists: true, errors: validateProjectConfig(config) };
|
|
70
73
|
}
|
|
71
74
|
|
|
72
75
|
export function configureActor(projectDir, { name, role = "", session = "default", provisioning = "existing", credentials = {}, replace = false } = {}) {
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
export const TAPP_DIRECTORY = ".tapp";
|
|
5
|
+
export const LEGACY_TAPP_DIRECTORY = ".autotap";
|
|
6
|
+
export const TAPP_CONFIG = ".tapp.yml";
|
|
7
|
+
export const LEGACY_TAPP_CONFIG = ".autotap.yml";
|
|
8
|
+
|
|
9
|
+
export function projectArtifactDirectory(projectDir, requested = TAPP_DIRECTORY) {
|
|
10
|
+
const root = path.resolve(projectDir);
|
|
11
|
+
if (requested !== TAPP_DIRECTORY) return requested;
|
|
12
|
+
const canonical = path.join(root, TAPP_DIRECTORY);
|
|
13
|
+
const legacy = path.join(root, LEGACY_TAPP_DIRECTORY);
|
|
14
|
+
if (!fs.existsSync(canonical) && fs.existsSync(legacy)) return LEGACY_TAPP_DIRECTORY;
|
|
15
|
+
return TAPP_DIRECTORY;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function projectArtifactPath(projectDir, ...parts) {
|
|
19
|
+
return path.join(path.resolve(projectDir), projectArtifactDirectory(projectDir), ...parts);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function existingProjectArtifactPath(projectDir, ...parts) {
|
|
23
|
+
const root = path.resolve(projectDir);
|
|
24
|
+
const canonical = path.join(root, TAPP_DIRECTORY, ...parts);
|
|
25
|
+
if (fs.existsSync(canonical)) return canonical;
|
|
26
|
+
const legacy = path.join(root, LEGACY_TAPP_DIRECTORY, ...parts);
|
|
27
|
+
return fs.existsSync(legacy) ? legacy : canonical;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function isProjectArtifactDirectory(name) {
|
|
31
|
+
return name === TAPP_DIRECTORY || name === LEGACY_TAPP_DIRECTORY;
|
|
32
|
+
}
|
package/mcp-server/src/report.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Pure report/gate logic shared by the MCP server (index.js) and the CI gate CLI
|
|
2
|
-
// (ci-report.js). Turns a capture's OCQA markers into the same ship/no-ship report the
|
|
2
|
+
// (ci-report.js). Turns a capture's OCQA markers into the same ship/no-ship report the Tapp
|
|
3
3
|
// app produces, and diffs two runs' findings into the CI regression gate. No shell, no server —
|
|
4
4
|
// keep it dependency-free so the CI path stays importable and testable.
|
|
5
5
|
import fs from "fs";
|
|
@@ -78,7 +78,7 @@ export function parseOcqaMarkers(markersFilePath) {
|
|
|
78
78
|
};
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
-
// Map harness OCQA_ISSUE `type` ->
|
|
81
|
+
// Map harness OCQA_ISSUE `type` -> Tapp FindingCategory. Crashes are always critical.
|
|
82
82
|
export const ISSUE_CATEGORY = {
|
|
83
83
|
crash: "crash",
|
|
84
84
|
app_hang: "app_hang",
|
|
@@ -100,7 +100,7 @@ export function severityRank(s) {
|
|
|
100
100
|
return { critical: 0, high: 1, medium: 2, low: 3 }[s] ?? 4;
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
-
// Turn a capture's OCQA markers into the same ship/no-ship report
|
|
103
|
+
// Turn a capture's OCQA markers into the same ship/no-ship report Tapp produces:
|
|
104
104
|
// deduped findings + a trustworthy verdict with a coverage floor (mirrors OrchestratorService).
|
|
105
105
|
export function buildQaReport(markersFilePath, { platform = "ios" } = {}) {
|
|
106
106
|
const base = parseOcqaMarkers(markersFilePath);
|
|
@@ -7,6 +7,7 @@ import path from "node:path";
|
|
|
7
7
|
import { spawnSync } from "node:child_process";
|
|
8
8
|
import { fileURLToPath } from "node:url";
|
|
9
9
|
import { semanticUiKey } from "./ui-map.js";
|
|
10
|
+
import { isProjectArtifactDirectory, projectArtifactDirectory } from "./project-paths.js";
|
|
10
11
|
|
|
11
12
|
const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
|
|
12
13
|
|
|
@@ -84,12 +85,15 @@ export function loadTaskFile(taskPath) {
|
|
|
84
85
|
return { ...task, __path: path.resolve(taskPath) };
|
|
85
86
|
}
|
|
86
87
|
|
|
87
|
-
function
|
|
88
|
-
if (explicitProjectDir)
|
|
88
|
+
function findTappDir(sourcePath, explicitProjectDir = "") {
|
|
89
|
+
if (explicitProjectDir) {
|
|
90
|
+
const root = path.resolve(explicitProjectDir);
|
|
91
|
+
return path.join(root, projectArtifactDirectory(root));
|
|
92
|
+
}
|
|
89
93
|
let current = path.dirname(path.resolve(sourcePath));
|
|
90
94
|
while (current !== path.dirname(current)) {
|
|
91
|
-
if (path.basename(current)
|
|
92
|
-
const candidate = path.join(current,
|
|
95
|
+
if (isProjectArtifactDirectory(path.basename(current))) return current;
|
|
96
|
+
const candidate = path.join(current, projectArtifactDirectory(current));
|
|
93
97
|
if (fs.existsSync(candidate)) return candidate;
|
|
94
98
|
current = path.dirname(current);
|
|
95
99
|
}
|
|
@@ -97,16 +101,16 @@ function findAutotapDir(sourcePath, explicitProjectDir = "") {
|
|
|
97
101
|
}
|
|
98
102
|
|
|
99
103
|
export function loadTaskRegistry({ sourcePath, projectDir = "", taskFiles = [] }) {
|
|
100
|
-
const
|
|
101
|
-
const taskDir =
|
|
104
|
+
const tappDir = findTappDir(sourcePath, projectDir);
|
|
105
|
+
const taskDir = tappDir ? path.join(tappDir, "tasks") : "";
|
|
102
106
|
const reviewed = taskDir && fs.existsSync(taskDir)
|
|
103
107
|
? fs.readdirSync(taskDir).filter((name) => /\.ya?ml$|\.json$/i.test(name)).map((name) => path.join(taskDir, name))
|
|
104
108
|
: [];
|
|
105
|
-
// Draft contracts generated under `.
|
|
109
|
+
// Draft contracts generated under `.tapp/proposals/contracts` may compile
|
|
106
110
|
// against sibling untrusted Task drafts. Ordinary committed contracts never
|
|
107
111
|
// see this directory, so a proposal cannot silently enter the release gate.
|
|
108
|
-
const proposalSource = String(path.resolve(sourcePath || "")).includes(`${path.sep}
|
|
109
|
-
const proposalDir = proposalSource &&
|
|
112
|
+
const proposalSource = [".tapp", ".autotap"].some((directory) => String(path.resolve(sourcePath || "")).includes(`${path.sep}${directory}${path.sep}proposals${path.sep}`));
|
|
113
|
+
const proposalDir = proposalSource && tappDir ? path.join(tappDir, "proposals", "tasks") : "";
|
|
110
114
|
const proposed = proposalDir && fs.existsSync(proposalDir)
|
|
111
115
|
? fs.readdirSync(proposalDir).filter((name) => /\.ya?ml$|\.json$/i.test(name)).map((name) => path.join(proposalDir, name))
|
|
112
116
|
: [];
|
|
@@ -169,7 +173,7 @@ export function compileTaskSteps({ steps, registry, platform = "", flowVars = {}
|
|
|
169
173
|
const invocation = taskCall(step);
|
|
170
174
|
if (!invocation) { expanded.push(step); continue; }
|
|
171
175
|
const task = registry.get(invocation.call.task);
|
|
172
|
-
if (!task) throw new Error(`Task '${invocation.call.task}' was not found in .
|
|
176
|
+
if (!task) throw new Error(`Task '${invocation.call.task}' was not found in .tapp/tasks`);
|
|
173
177
|
if (stack.includes(task.name)) throw new Error(`Task cycle detected: ${[...stack, task.name].join(" -> ")}`);
|
|
174
178
|
const implementation = implementationSteps(task, platform);
|
|
175
179
|
if (!implementation?.steps) throw new Error(`Task '${task.name}' has no '${platform || "shared"}' implementation`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aarwitz/tapp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.0",
|
|
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",
|
|
@@ -24,7 +24,19 @@
|
|
|
24
24
|
"docs/application-model.md",
|
|
25
25
|
"docs/scenarios.md",
|
|
26
26
|
"mcp-server/src/",
|
|
27
|
-
"scripts/",
|
|
27
|
+
"scripts/ci-gate.sh",
|
|
28
|
+
"scripts/compile-contract.js",
|
|
29
|
+
"scripts/compile-flow.js",
|
|
30
|
+
"scripts/flow-platform.js",
|
|
31
|
+
"scripts/flow_ai_judge.py",
|
|
32
|
+
"scripts/flow_lib.py",
|
|
33
|
+
"scripts/platform-gate.js",
|
|
34
|
+
"scripts/pr-plan.js",
|
|
35
|
+
"scripts/quick-capture.sh",
|
|
36
|
+
"scripts/run-android-flow.js",
|
|
37
|
+
"scripts/run-flow.sh",
|
|
38
|
+
"scripts/run-web-flow.js",
|
|
39
|
+
"scripts/run-web-scenario.js",
|
|
28
40
|
"Harness/OCQAHarness/",
|
|
29
41
|
"Harness/OCQAHarnessUITests/",
|
|
30
42
|
"Harness/OCQAHarness.xcodeproj/",
|
|
@@ -75,7 +87,7 @@
|
|
|
75
87
|
"mobile"
|
|
76
88
|
],
|
|
77
89
|
"scripts": {
|
|
78
|
-
"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/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",
|
|
79
91
|
"test:browser-journey": "node --test tests/browser-journey.test.js",
|
|
80
92
|
"test:browser-native": "TAPP_RUN_NATIVE_BROWSER=1 node --test tests/browser-native-journey.test.js"
|
|
81
93
|
}
|
package/scripts/ci-gate.sh
CHANGED
|
@@ -15,9 +15,9 @@
|
|
|
15
15
|
# # bundle id is detected from the .app when omitted
|
|
16
16
|
# [--actions N] # exploration budget (default 40)
|
|
17
17
|
# [--timeout S] # exploration watchdog (default 600)
|
|
18
|
-
# [--flows <glob>] # Flow YAMLs to replay (default: <app repo>/.
|
|
19
|
-
# [--scenarios <glob>] # Multi-actor Scenario YAMLs (web; default: <app repo>/.
|
|
20
|
-
# [--contracts <glob>] # TypeScript release contracts (default: <app repo>/.
|
|
18
|
+
# [--flows <glob>] # Flow YAMLs to replay (default: <app repo>/.tapp/flows/*.yml if --project-dir given)
|
|
19
|
+
# [--scenarios <glob>] # Multi-actor Scenario YAMLs (web; default: <app repo>/.tapp/scenarios/*.yml)
|
|
20
|
+
# [--contracts <glob>] # TypeScript release contracts (default: <app repo>/.tapp/contracts/*.contract.ts)
|
|
21
21
|
# [--project-dir <dir>] # the app repo checkout (for flows + baseline defaults)
|
|
22
22
|
# [--pr-base <git-ref>] # select critical + diff-relevant contracts from base...head
|
|
23
23
|
# [--pr-head <git-ref>] # default HEAD
|
|
@@ -79,10 +79,15 @@ 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)"
|
|
81
81
|
fi
|
|
82
|
-
|
|
83
|
-
[[
|
|
84
|
-
[[ -
|
|
85
|
-
[[ -z "$
|
|
82
|
+
TAPP_PROJECT_ARTIFACTS=""
|
|
83
|
+
if [[ -n "$PROJECT_DIR" ]]; then
|
|
84
|
+
[[ -d "$PROJECT_DIR/.tapp" ]] && TAPP_PROJECT_ARTIFACTS="$PROJECT_DIR/.tapp"
|
|
85
|
+
[[ -z "$TAPP_PROJECT_ARTIFACTS" && -d "$PROJECT_DIR/.autotap" ]] && TAPP_PROJECT_ARTIFACTS="$PROJECT_DIR/.autotap"
|
|
86
|
+
fi
|
|
87
|
+
[[ -z "$FLOWS" && -n "$TAPP_PROJECT_ARTIFACTS" && -d "$TAPP_PROJECT_ARTIFACTS/flows" ]] && FLOWS="$TAPP_PROJECT_ARTIFACTS/flows/*.yml"
|
|
88
|
+
[[ "$PLATFORM" == "web" && -z "$SCENARIOS" && -n "$TAPP_PROJECT_ARTIFACTS" && -d "$TAPP_PROJECT_ARTIFACTS/scenarios" ]] && SCENARIOS="$TAPP_PROJECT_ARTIFACTS/scenarios/*.yml"
|
|
89
|
+
[[ -z "$CONTRACTS" && -n "$TAPP_PROJECT_ARTIFACTS" && -d "$TAPP_PROJECT_ARTIFACTS/contracts" ]] && CONTRACTS="$TAPP_PROJECT_ARTIFACTS/contracts/*.contract.ts"
|
|
90
|
+
[[ -z "$BASELINE" && -n "$TAPP_PROJECT_ARTIFACTS" && -f "$TAPP_PROJECT_ARTIFACTS/baseline.json" ]] && BASELINE="$TAPP_PROJECT_ARTIFACTS/baseline.json"
|
|
86
91
|
[[ -z "$BASELINE" || -f "$BASELINE" ]] || { echo "❌ Baseline not found: $BASELINE" >&2; exit 2; }
|
|
87
92
|
if [[ -n "$BASELINE" ]]; then
|
|
88
93
|
python3 -c 'import json,sys; json.load(open(sys.argv[1]))' "$BASELINE" 2>/dev/null \
|
|
@@ -254,7 +259,7 @@ print(named[0] if named else (iphones[0] if iphones else ''))
|
|
|
254
259
|
if [[ -z "$UDID" ]]; then
|
|
255
260
|
RUNTIME="$(xcrun simctl list runtimes -j | python3 -c 'import sys,json; rts=[r for r in json.load(sys.stdin)["runtimes"] if r.get("isAvailable") and r["platform"]=="iOS"]; print(rts[-1]["identifier"] if rts else "")')"
|
|
256
261
|
[[ -n "$RUNTIME" ]] || { echo "❌ No iOS simulator runtime available" >&2; exit 1; }
|
|
257
|
-
UDID="$(xcrun simctl create "
|
|
262
|
+
UDID="$(xcrun simctl create "Tapp CI" "$DEVICE" "$RUNTIME")" || { echo "❌ Could not create simulator" >&2; exit 1; }
|
|
258
263
|
fi
|
|
259
264
|
echo "Booting $UDID …"
|
|
260
265
|
xcrun simctl boot "$UDID" || true
|
|
@@ -269,7 +274,7 @@ xcrun simctl install "$UDID" "$APP_PATH" || { echo "❌ simctl install failed
|
|
|
269
274
|
# ── Autonomous exploration (quick-capture builds the harness itself if needed).
|
|
270
275
|
step "Explore ($ACTIONS actions, ${TIMEOUT}s watchdog)"
|
|
271
276
|
set +e
|
|
272
|
-
CAPTURE_ROOT="${AUTOTAP_HOME:-$ROOT}/captures"
|
|
277
|
+
CAPTURE_ROOT="${TAPP_HOME:-${AUTOTAP_HOME:-$ROOT}}/captures"
|
|
273
278
|
mkdir -p "$CAPTURE_ROOT"
|
|
274
279
|
CAPTURE_DIR="$(mktemp -d "$CAPTURE_ROOT/ci.XXXXXX")"
|
|
275
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"
|
|
@@ -282,7 +287,7 @@ echo "Markers: $MARKERS"
|
|
|
282
287
|
FLOW_LOG_ARGS=()
|
|
283
288
|
if [[ "${#FLOW_FILES[@]}" -gt 0 ]]; then
|
|
284
289
|
step "Flows"
|
|
285
|
-
FLOW_LOG_DIR="$(mktemp -d /tmp/
|
|
290
|
+
FLOW_LOG_DIR="$(mktemp -d /tmp/tapp-ci-flows.XXXXXX)"
|
|
286
291
|
for flow in "${FLOW_FILES[@]}"; do
|
|
287
292
|
name="$(basename "$flow" .yml)"
|
|
288
293
|
log="$FLOW_LOG_DIR/$name.log"
|
package/scripts/flow_ai_judge.py
CHANGED
|
@@ -26,7 +26,7 @@ def _ctx():
|
|
|
26
26
|
|
|
27
27
|
|
|
28
28
|
SSL_CTX = _ctx()
|
|
29
|
-
MODEL = os.environ.get("AUTOTAP_VISION_MODEL", "claude-haiku-4-5-20251001")
|
|
29
|
+
MODEL = os.environ.get("TAPP_VISION_MODEL") or os.environ.get("AUTOTAP_VISION_MODEL", "claude-haiku-4-5-20251001")
|
|
30
30
|
SYSTEM = (
|
|
31
31
|
"You are a QA test oracle. You are shown one iOS app screenshot and a CLAIM the test asserts about "
|
|
32
32
|
"it. Decide if the claim is TRUE of what is actually visible. Be strict and literal — only pass if "
|
package/scripts/flow_lib.py
CHANGED
|
@@ -23,7 +23,7 @@ def load_flow(path):
|
|
|
23
23
|
|
|
24
24
|
|
|
25
25
|
def to_yaml(json_str):
|
|
26
|
-
"""Emit a recorded/inline Flow (JSON string) as tidy YAML for saving to .
|
|
26
|
+
"""Emit a recorded/inline Flow (JSON string) as tidy YAML for saving to .tapp/flows/*.yml."""
|
|
27
27
|
import yaml
|
|
28
28
|
flow = json.loads(json_str)
|
|
29
29
|
# Order keys for readability. `platform` + `url` make the same repository-native
|
package/scripts/quick-capture.sh
CHANGED
|
@@ -12,27 +12,28 @@ set -euo pipefail
|
|
|
12
12
|
# [--timeout <secs>] # Hard timeout for explore command
|
|
13
13
|
# quick-capture.sh tree <bundleId> # Dump accessibility tree
|
|
14
14
|
#
|
|
15
|
-
# All output goes to
|
|
15
|
+
# All output goes to the configured Tapp home or the repository capture directory.
|
|
16
16
|
|
|
17
17
|
PROJECT_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
|
18
18
|
# TAPP_CAPTURE_DIR selects one exact run directory (used by CI so a failed run can never
|
|
19
|
-
# accidentally reuse stale markers).
|
|
20
|
-
# set by the `
|
|
19
|
+
# accidentally reuse stale markers). TAPP_HOME redirects all writable output otherwise —
|
|
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:-${AUTOTAP_HOME:-}}"
|
|
22
23
|
if [[ -n "${TAPP_CAPTURE_DIR:-}" ]]; then
|
|
23
24
|
CAPTURE_DIR="$TAPP_CAPTURE_DIR"
|
|
24
|
-
elif [[ -n "$
|
|
25
|
-
CAPTURE_DIR="$
|
|
25
|
+
elif [[ -n "$TAPP_RUNTIME_HOME" ]]; then
|
|
26
|
+
CAPTURE_DIR="$TAPP_RUNTIME_HOME/captures/$(date +%Y%m%d-%H%M%S)"
|
|
26
27
|
else
|
|
27
28
|
CAPTURE_DIR="$PROJECT_ROOT/captures/$(date +%Y%m%d-%H%M%S)"
|
|
28
29
|
fi
|
|
29
|
-
if [[ -n "$
|
|
30
|
-
HARNESS_DERIVED="$
|
|
30
|
+
if [[ -n "$TAPP_RUNTIME_HOME" ]]; then
|
|
31
|
+
HARNESS_DERIVED="$TAPP_RUNTIME_HOME/harness-derived"
|
|
31
32
|
else
|
|
32
|
-
HARNESS_DERIVED="/tmp/
|
|
33
|
+
HARNESS_DERIVED="/tmp/tapp-harness-derived"
|
|
33
34
|
fi
|
|
34
35
|
HARNESS_PROJECT="$PROJECT_ROOT/Harness/OCQAHarness.xcodeproj"
|
|
35
|
-
DEFAULT_BUNDLE="
|
|
36
|
+
DEFAULT_BUNDLE="io.github.aarwitz.tapp.demoapp"
|
|
36
37
|
|
|
37
38
|
get_booted_sim() {
|
|
38
39
|
xcrun simctl list devices booted -j 2>/dev/null \
|
|
@@ -168,13 +169,13 @@ run_harness_test() {
|
|
|
168
169
|
"OCQA_MAX_ACTIONS": "$max_actions",
|
|
169
170
|
"OCQA_TIMEOUT_SECONDS": "$timeout_secs",
|
|
170
171
|
"OCQA_TEST_EMAIL": "${OCQA_TEST_EMAIL:-qa@example.com}",
|
|
171
|
-
"OCQA_TEST_PASSWORD": "${OCQA_TEST_PASSWORD:-
|
|
172
|
+
"OCQA_TEST_PASSWORD": "${OCQA_TEST_PASSWORD:-Tapp123!}"$interactive_line$overrides_line$launch_args_line$launch_env_line$login_steps_line$pr_target_line
|
|
172
173
|
}
|
|
173
174
|
CONF
|
|
174
175
|
|
|
175
176
|
local xctestrun=$(find "$HARNESS_DERIVED/Build/Products" -name "*.xctestrun" 2>/dev/null | head -1)
|
|
176
177
|
if [[ -z "$xctestrun" ]]; then
|
|
177
|
-
echo "ERROR: No xctestrun found. Run:
|
|
178
|
+
echo "ERROR: No xctestrun found. Run: tapp install" >&2
|
|
178
179
|
return 1
|
|
179
180
|
fi
|
|
180
181
|
|
|
@@ -382,12 +383,12 @@ case "$MODE" in
|
|
|
382
383
|
"OCQA_SESSION_RESULT_PATH": "$SESS_RES",
|
|
383
384
|
"OCQA_SESSION_TIMEOUT": "$SESS_TIMEOUT",
|
|
384
385
|
"OCQA_TEST_EMAIL": "${OCQA_TEST_EMAIL:-qa@example.com}",
|
|
385
|
-
"OCQA_TEST_PASSWORD": "${OCQA_TEST_PASSWORD:-
|
|
386
|
+
"OCQA_TEST_PASSWORD": "${OCQA_TEST_PASSWORD:-Tapp123!}"$sess_args_line$sess_env_line
|
|
386
387
|
}
|
|
387
388
|
CONF
|
|
388
389
|
xctestrun=$(find "$HARNESS_DERIVED/Build/Products" -name "*.xctestrun" 2>/dev/null | head -1)
|
|
389
390
|
if [[ -z "$xctestrun" ]]; then
|
|
390
|
-
echo "ERROR: No xctestrun. Run:
|
|
391
|
+
echo "ERROR: No xctestrun. Run: tapp install" >&2
|
|
391
392
|
exit 1
|
|
392
393
|
fi
|
|
393
394
|
export TEST_RUNNER_OCQA_CONFIG_PATH=/tmp/ocqa-run-config.json
|
|
@@ -399,7 +400,7 @@ CONF
|
|
|
399
400
|
;;
|
|
400
401
|
|
|
401
402
|
build-harness)
|
|
402
|
-
# Just (re)build the harness cache for the booted sim — used by `
|
|
403
|
+
# Just (re)build the harness cache for the booted sim — used by `tapp install`
|
|
403
404
|
# for a fast first tool call later. No capture output.
|
|
404
405
|
ensure_harness_built "$SIM_NAME"
|
|
405
406
|
rmdir "$CAPTURE_DIR" 2>/dev/null || true
|
package/scripts/run-flow.sh
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
-
# Run
|
|
2
|
+
# Run a Tapp Flow — a deterministic, authored/recorded E2E test.
|
|
3
3
|
#
|
|
4
4
|
# Replays the flow's steps against an app on the BOOTED simulator with wait-for-condition timing and
|
|
5
5
|
# poll-with-timeout assertions (no sleeps), then prints a scannable pass/fail report. Deterministic by
|
|
@@ -24,14 +24,14 @@ APP="${2:-}"
|
|
|
24
24
|
|
|
25
25
|
UDID="$(xcrun simctl list devices booted -j 2>/dev/null | python3 -c 'import sys,json; d=json.load(sys.stdin); print(next((x["udid"] for v in d["devices"].values() for x in v if x.get("state")=="Booted"), ""))')"
|
|
26
26
|
[ -z "$UDID" ] && { echo "❌ No booted simulator."; exit 2; }
|
|
27
|
-
# When run through the `
|
|
28
|
-
# AUTOTAP_HOME — check there first, then the repo-dev locations.
|
|
27
|
+
# When run through the installed `tapp` CLI, the harness cache lives under TAPP_HOME.
|
|
29
28
|
XCTR=""
|
|
30
|
-
|
|
29
|
+
TAPP_RUNTIME_HOME="${TAPP_HOME:-${AUTOTAP_HOME:-}}"
|
|
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
|
-
[ -z "$XCTR" ] && XCTR="$(find /tmp/
|
|
32
|
+
[ -z "$XCTR" ] && XCTR="$(find /tmp/tapp-harness-derived/Build/Products -name '*.xctestrun' 2>/dev/null | head -1)"
|
|
33
33
|
[ -z "$XCTR" ] && XCTR="$(find /tmp/harness-build/Build/Products -name '*.xctestrun' 2>/dev/null | head -1)"
|
|
34
|
-
[ -z "$XCTR" ] && { echo "❌ Harness not built. Run:
|
|
34
|
+
[ -z "$XCTR" ] && { echo "❌ Harness not built. Run: tapp install"; exit 2; }
|
|
35
35
|
|
|
36
36
|
NAME="$(python3 -c "import sys,json;print(json.loads(sys.argv[1]).get('name','flow'))" "$FLOW_JSON")"
|
|
37
37
|
echo "▶️ Running flow \"$NAME\" against $APP …"
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
# Build and exercise every native Android fixture through the same release gate
|
|
3
|
-
# customers use. A connected, booted emulator/device is required.
|
|
4
|
-
set -euo pipefail
|
|
5
|
-
|
|
6
|
-
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
|
7
|
-
CORPUS="$ROOT/AndroidCorpus"
|
|
8
|
-
SERIAL="${1:-${ANDROID_SERIAL:-}}"
|
|
9
|
-
SERIAL_ARGS=()
|
|
10
|
-
[[ -n "$SERIAL" ]] && SERIAL_ARGS=(--serial "$SERIAL")
|
|
11
|
-
|
|
12
|
-
"$CORPUS/gradlew" -p "$CORPUS" \
|
|
13
|
-
:demoapp:assembleDebug :logindemo:assembleDebug :shopdemo:assembleDebug --no-daemon --quiet
|
|
14
|
-
|
|
15
|
-
run_fixture() {
|
|
16
|
-
local module="$1" app_id="$2" flow="$3"
|
|
17
|
-
local apk="$CORPUS/$module/build/outputs/apk/debug/$module-debug.apk"
|
|
18
|
-
local output_dir="${RUNNER_TEMP:-/tmp}/tapp-android-corpus-$module"
|
|
19
|
-
mkdir -p "$output_dir"
|
|
20
|
-
echo "Testing Android corpus fixture: $module ($app_id)"
|
|
21
|
-
"$ROOT/scripts/ci-gate.sh" --platform android --apk "$apk" --app-id "$app_id" \
|
|
22
|
-
"${SERIAL_ARGS[@]}" --actions 12 --timeout 180 --flows "$CORPUS/$module/.autotap/flows/$flow" \
|
|
23
|
-
--project-dir "$CORPUS/$module" --json-out "$output_dir/report.json" --md-out "$output_dir/report.md"
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
run_fixture demoapp io.tapp.corpus.demo smoke.yml
|
|
27
|
-
run_fixture logindemo io.tapp.corpus.login sign-in.yml
|
|
28
|
-
run_fixture shopdemo io.tapp.corpus.shop checkout.yml
|
|
29
|
-
|
|
30
|
-
echo "All Android corpus gates passed."
|