@aarwitz/tapp 0.15.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 +123 -0
- package/Harness/OCQAHarness/AppDelegate.swift +21 -0
- package/Harness/OCQAHarness/Info.plist +26 -0
- package/Harness/OCQAHarness.xcodeproj/project.pbxproj +199 -0
- package/Harness/OCQAHarness.xcodeproj/xcshareddata/xcschemes/OCQAHarnessUITests.xcscheme +22 -0
- package/Harness/OCQAHarnessUITests/ExplorerTests.swift +4526 -0
- package/Harness/OCQAHarnessUITests/Info.plist +22 -0
- package/Harness/generate-harness-xcodeproj.rb +254 -0
- package/LICENSE +21 -0
- package/README.md +374 -0
- package/bin/tapp.js +1382 -0
- package/browser/app.css +227 -0
- package/browser/app.js +675 -0
- package/browser/index.html +195 -0
- package/browser/product-contract.js +25 -0
- package/browser/view-model.js +16 -0
- package/docs/BROWSER-PRODUCT.md +72 -0
- package/docs/PRODUCT-ENGINE.md +102 -0
- package/docs/application-model.md +276 -0
- package/docs/scenarios.md +95 -0
- package/mcp-server/src/android-driver.js +287 -0
- package/mcp-server/src/android-explorer.js +197 -0
- package/mcp-server/src/android-flow.js +89 -0
- package/mcp-server/src/application-model.js +1597 -0
- package/mcp-server/src/browser-product.js +659 -0
- package/mcp-server/src/browser-workspaces.js +234 -0
- package/mcp-server/src/ci-report.js +557 -0
- package/mcp-server/src/ci-setup.js +359 -0
- package/mcp-server/src/contract-authoring.js +10 -0
- package/mcp-server/src/enrich.js +57 -0
- package/mcp-server/src/flow-runtime.js +127 -0
- package/mcp-server/src/html-report.js +124 -0
- package/mcp-server/src/index.js +3775 -0
- package/mcp-server/src/maintenance-proposal.js +178 -0
- package/mcp-server/src/managed-operation.js +61 -0
- package/mcp-server/src/pr-selection.js +841 -0
- package/mcp-server/src/product-execution.js +155 -0
- package/mcp-server/src/product-operations.js +526 -0
- package/mcp-server/src/project-config.js +101 -0
- package/mcp-server/src/release-contract.d.ts +81 -0
- package/mcp-server/src/release-contract.js +226 -0
- package/mcp-server/src/report.js +363 -0
- package/mcp-server/src/scenario-runtime.js +139 -0
- package/mcp-server/src/static-server.js +44 -0
- package/mcp-server/src/task-runtime.js +266 -0
- package/mcp-server/src/ui-map.js +661 -0
- package/mcp-server/src/web-explorer.js +493 -0
- package/mcp-server/src/web-flow.js +238 -0
- package/package.json +82 -0
- package/scripts/android-corpus-e2e.sh +30 -0
- package/scripts/ci-gate.sh +323 -0
- package/scripts/cleanup-xcode.sh +157 -0
- package/scripts/compile-contract.js +27 -0
- package/scripts/compile-flow.js +18 -0
- package/scripts/corpus-apps.txt +9 -0
- package/scripts/corpus-sweep.sh +121 -0
- package/scripts/coverage-eval.sh +92 -0
- package/scripts/coverage_eval_parse.py +95 -0
- package/scripts/deploy-and-build.sh +99 -0
- package/scripts/flow-platform.js +18 -0
- package/scripts/flow_ai_judge.py +102 -0
- package/scripts/flow_lib.py +154 -0
- package/scripts/mutation-recall-desktop.sh +186 -0
- package/scripts/mutation-recall.sh +121 -0
- package/scripts/mutation_lib.py +128 -0
- package/scripts/mutation_operators.py +144 -0
- package/scripts/platform-gate.js +186 -0
- package/scripts/pr-plan.js +68 -0
- package/scripts/quick-capture.sh +419 -0
- package/scripts/run-android-flow.js +27 -0
- package/scripts/run-flow.sh +90 -0
- package/scripts/run-web-flow.js +28 -0
- package/scripts/run-web-scenario.js +23 -0
- package/scripts/validation-matrix.sh +146 -0
- package/scripts/vision-fp-eval.sh +206 -0
- package/scripts/vision_escalation_responder.py +147 -0
- package/scripts/vision_fp_probe.py +221 -0
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
// Shared deterministic execution boundary for customer-facing product operations.
|
|
2
|
+
// CLI, MCP, browser, and CI adapters should call this instead of invoking one
|
|
3
|
+
// another. The platform scripts remain low-level executors and all evidence is
|
|
4
|
+
// written outside the package directory.
|
|
5
|
+
import { spawn } from "node:child_process";
|
|
6
|
+
import crypto from "node:crypto";
|
|
7
|
+
import fs from "node:fs";
|
|
8
|
+
import os from "node:os";
|
|
9
|
+
import path from "node:path";
|
|
10
|
+
import { fileURLToPath } from "node:url";
|
|
11
|
+
import { compileReleaseContract, loadReleaseContractFile } from "./release-contract.js";
|
|
12
|
+
|
|
13
|
+
const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
|
|
14
|
+
|
|
15
|
+
function executionHome() {
|
|
16
|
+
return process.env.AUTOTAP_HOME || process.env.TAPP_HOME || path.join(os.homedir(), ".tapp");
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function atomicJson(destination, value) {
|
|
20
|
+
fs.mkdirSync(path.dirname(destination), { recursive: true });
|
|
21
|
+
const temporary = `${destination}.tmp-${process.pid}-${Date.now()}`;
|
|
22
|
+
fs.writeFileSync(temporary, JSON.stringify(value, null, 2) + "\n", { mode: 0o600 });
|
|
23
|
+
fs.renameSync(temporary, destination);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function boundedAppend(value, chunk, maximum = 2 * 1024 * 1024) {
|
|
27
|
+
const next = value + String(chunk || "");
|
|
28
|
+
return next.length > maximum ? next.slice(next.length - maximum) : next;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function runProductProcess(command, args, { cwd, env = process.env, timeoutMs = 900_000, onOutput = () => {} } = {}) {
|
|
32
|
+
return new Promise((resolve) => {
|
|
33
|
+
const child = spawn(command, args, { cwd, env, shell: false, stdio: ["ignore", "pipe", "pipe"] });
|
|
34
|
+
let stdout = "";
|
|
35
|
+
let stderr = "";
|
|
36
|
+
let timedOut = false;
|
|
37
|
+
const timer = setTimeout(() => {
|
|
38
|
+
timedOut = true;
|
|
39
|
+
try { child.kill("SIGTERM"); } catch {}
|
|
40
|
+
setTimeout(() => { try { child.kill("SIGKILL"); } catch {} }, 3000).unref();
|
|
41
|
+
}, Math.max(1000, Number(timeoutMs) || 900_000));
|
|
42
|
+
timer.unref?.();
|
|
43
|
+
child.stdout.on("data", (chunk) => {
|
|
44
|
+
stdout = boundedAppend(stdout, chunk);
|
|
45
|
+
onOutput({ stream: "stdout", text: String(chunk) });
|
|
46
|
+
});
|
|
47
|
+
child.stderr.on("data", (chunk) => {
|
|
48
|
+
stderr = boundedAppend(stderr, chunk);
|
|
49
|
+
onOutput({ stream: "stderr", text: String(chunk) });
|
|
50
|
+
});
|
|
51
|
+
child.once("error", (error) => {
|
|
52
|
+
clearTimeout(timer);
|
|
53
|
+
resolve({ code: 1, stdout, stderr: boundedAppend(stderr, error.message), timedOut, error: error.message });
|
|
54
|
+
});
|
|
55
|
+
child.once("close", (code, signal) => {
|
|
56
|
+
clearTimeout(timer);
|
|
57
|
+
resolve({ code: code ?? 1, signal, stdout, stderr, timedOut });
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function executionInvocation({ compiledPath, compiled, platform, url, bundleId, appId, apkPath, serial }) {
|
|
63
|
+
if (compiled.kind === "scenario") {
|
|
64
|
+
if (platform !== "web") throw new Error("Isolated multi-actor Scenario replay currently requires the web platform");
|
|
65
|
+
return [process.execPath, [path.join(packageRoot, "scripts", "run-web-scenario.js"), compiledPath, url || compiled.url || compiled.app || ""]];
|
|
66
|
+
}
|
|
67
|
+
if (platform === "web") {
|
|
68
|
+
const targetUrl = url || compiled.url || compiled.app || "";
|
|
69
|
+
if (!targetUrl) throw new Error("Browser contract replay needs an owned URL or managed web target");
|
|
70
|
+
return [process.execPath, [path.join(packageRoot, "scripts", "run-web-flow.js"), compiledPath, targetUrl]];
|
|
71
|
+
}
|
|
72
|
+
if (platform === "android") {
|
|
73
|
+
const targetApp = appId || compiled.app || "";
|
|
74
|
+
if (!targetApp) throw new Error("Android contract replay needs an application id");
|
|
75
|
+
return [process.execPath, [path.join(packageRoot, "scripts", "run-android-flow.js"), compiledPath, targetApp, apkPath || "", serial || ""]];
|
|
76
|
+
}
|
|
77
|
+
const targetApp = bundleId || compiled.app || "";
|
|
78
|
+
if (!targetApp) throw new Error("iOS contract replay needs a bundle id");
|
|
79
|
+
return ["bash", [path.join(packageRoot, "scripts", "run-flow.sh"), compiledPath, targetApp]];
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export async function executeReleaseContract({
|
|
83
|
+
projectDir,
|
|
84
|
+
contractPath,
|
|
85
|
+
platform,
|
|
86
|
+
url = "",
|
|
87
|
+
bundleId = "",
|
|
88
|
+
appId = "",
|
|
89
|
+
apkPath = "",
|
|
90
|
+
serial = "",
|
|
91
|
+
testEmail,
|
|
92
|
+
testPassword,
|
|
93
|
+
timeout = 600,
|
|
94
|
+
onOutput = () => {},
|
|
95
|
+
} = {}) {
|
|
96
|
+
const root = fs.realpathSync(path.resolve(projectDir || process.cwd()));
|
|
97
|
+
const source = path.resolve(root, contractPath || "");
|
|
98
|
+
if (source !== root && !source.startsWith(root + path.sep)) throw new Error("Contract path must remain inside the repository");
|
|
99
|
+
if (!fs.existsSync(source)) throw new Error(`Release contract not found: ${source}`);
|
|
100
|
+
const contract = await loadReleaseContractFile(source);
|
|
101
|
+
const selectedPlatform = String(platform || (contract.platforms.length === 1 ? contract.platforms[0] : "")).toLowerCase();
|
|
102
|
+
if (!["ios", "android", "web"].includes(selectedPlatform)) throw new Error("A concrete ios|android|web platform is required");
|
|
103
|
+
if (!contract.platforms.includes(selectedPlatform)) throw new Error(`Release contract '${contract.name}' does not apply to ${selectedPlatform}`);
|
|
104
|
+
const compiled = compileReleaseContract(contract, { platform: selectedPlatform, sourcePath: source });
|
|
105
|
+
const token = `${Date.now()}-${crypto.randomBytes(5).toString("hex")}`;
|
|
106
|
+
const runDir = path.join(executionHome(), "product-execution", token);
|
|
107
|
+
const compiledPath = path.join(runDir, `${contract.name}.json`);
|
|
108
|
+
const logPath = path.join(runDir, "flow.log");
|
|
109
|
+
const evidenceKind = compiled.kind === "scenario" ? "scenario-web" : `flow-${selectedPlatform}`;
|
|
110
|
+
const evidenceDir = path.join(executionHome(), "captures", `${evidenceKind}-${token}`);
|
|
111
|
+
atomicJson(compiledPath, compiled);
|
|
112
|
+
const [command, args] = executionInvocation({ compiledPath, compiled, platform: selectedPlatform, url, bundleId, appId, apkPath, serial });
|
|
113
|
+
const env = {
|
|
114
|
+
...process.env,
|
|
115
|
+
FLOW_LOG: logPath,
|
|
116
|
+
TAPP_FLOW_EVIDENCE_DIR: evidenceDir,
|
|
117
|
+
...(typeof testEmail === "string" ? { OCQA_TEST_EMAIL: testEmail } : {}),
|
|
118
|
+
...(typeof testPassword === "string" ? { OCQA_TEST_PASSWORD: testPassword } : {}),
|
|
119
|
+
};
|
|
120
|
+
const preparation = selectedPlatform === "ios"
|
|
121
|
+
? await runProductProcess("bash", [path.join(packageRoot, "scripts", "quick-capture.sh"), "build-harness"], {
|
|
122
|
+
cwd: root,
|
|
123
|
+
env,
|
|
124
|
+
timeoutMs: 10 * 60 * 1000,
|
|
125
|
+
onOutput,
|
|
126
|
+
})
|
|
127
|
+
: { code: 0, stdout: "", stderr: "", timedOut: false };
|
|
128
|
+
const result = preparation.code === 0
|
|
129
|
+
? await runProductProcess(command, args, {
|
|
130
|
+
cwd: root,
|
|
131
|
+
env,
|
|
132
|
+
timeoutMs: Math.max(30, Math.min(3600, Number(timeout) || 600)) * 1000 + 30_000,
|
|
133
|
+
onOutput,
|
|
134
|
+
})
|
|
135
|
+
: preparation;
|
|
136
|
+
let report = "";
|
|
137
|
+
if (compiled.kind === "scenario" && fs.existsSync(logPath)) {
|
|
138
|
+
const rendered = await runProductProcess("python3", [path.join(packageRoot, "scripts", "flow_lib.py"), "report", logPath], { cwd: root, timeoutMs: 30_000, onOutput });
|
|
139
|
+
report = rendered.stdout;
|
|
140
|
+
}
|
|
141
|
+
return {
|
|
142
|
+
passed: result.code === 0,
|
|
143
|
+
code: result.code,
|
|
144
|
+
timedOut: result.timedOut,
|
|
145
|
+
contract: { name: contract.name, title: contract.title, criticality: contract.criticality },
|
|
146
|
+
platform: selectedPlatform,
|
|
147
|
+
kind: compiled.kind,
|
|
148
|
+
deterministicSteps: compiled.steps.length,
|
|
149
|
+
stdout: [preparation.code === 0 ? preparation.stdout : "", result.stdout, report].filter(Boolean).join("\n").trim(),
|
|
150
|
+
stderr: result.stderr.trim(),
|
|
151
|
+
evidence: evidenceDir,
|
|
152
|
+
logPath,
|
|
153
|
+
compiledPath,
|
|
154
|
+
};
|
|
155
|
+
}
|