agent-scenario-loop 0.1.3 → 0.1.4
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/app/profile-session.ts +263 -17
- package/dist/core/artifact-contract.d.ts +6 -4
- package/dist/core/artifact-contract.js +164 -15
- package/dist/core/schema-validator.d.ts +1 -0
- package/dist/core/schema-validator.js +1 -0
- package/dist/runner/android-adb-driver.d.ts +7 -2
- package/dist/runner/android-adb-driver.js +7 -1
- package/dist/runner/android-adb.d.ts +40 -5
- package/dist/runner/android-adb.js +1046 -664
- package/dist/runner/ios-simctl.d.ts +1 -0
- package/dist/runner/ios-simctl.js +1 -0
- package/dist/runner/profile-android.d.ts +11 -1
- package/dist/runner/profile-android.js +230 -16
- package/dist/runner/profile-ios.d.ts +3 -2
- package/dist/runner/profile-ios.js +223 -20
- package/dist/runner/profile-mobile.d.ts +31 -3
- package/dist/runner/profile-mobile.js +793 -20
- package/dist/runner/validate-project.js +3 -0
- package/dist/scripts/consumer-rehearsal.d.ts +119 -0
- package/dist/scripts/consumer-rehearsal.js +757 -0
- package/dist/scripts/downstream-local-package-gate.d.ts +2 -0
- package/dist/scripts/downstream-local-package-gate.js +264 -0
- package/dist/scripts/package-smoke.d.ts +96 -0
- package/dist/scripts/package-smoke.js +2282 -0
- package/dist/scripts/release-readiness.d.ts +2 -0
- package/dist/scripts/release-readiness.js +520 -0
- package/docs/adapters.md +3 -1
- package/docs/api.md +2 -2
- package/docs/authoring.md +34 -2
- package/docs/consumer-rehearsal.md +27 -1
- package/docs/contracts.md +16 -2
- package/docs/live-proofs.md +5 -3
- package/examples/mobile-app/runner-manifests/evidence-provider.json +3 -3
- package/examples/mobile-app/scripts/asl-capture-profiler-provider.mjs +25 -0
- package/examples/runners/README.md +3 -3
- package/examples/runners/axe-accessibility-provider.json +2 -2
- package/examples/runners/script-accessibility-provider.json +2 -2
- package/examples/runners/script-memory-provider.json +2 -2
- package/examples/runners/script-network-provider.json +2 -2
- package/examples/runners/script-profiler-provider.json +2 -2
- package/package.json +11 -3
- package/schemas/manifest.schema.json +73 -3
- package/schemas/profiler.schema.json +243 -0
- package/schemas/runner-capabilities.schema.json +8 -2
- package/schemas/scenario.schema.json +18 -2
- package/templates/evidence-provider.json +3 -3
- package/templates/scripts/asl-capture-profiler-provider.mjs +20 -0
|
@@ -0,0 +1,757 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.createRehearsalEnv = createRehearsalEnv;
|
|
5
|
+
exports.mergeGeneratedScripts = mergeGeneratedScripts;
|
|
6
|
+
exports.packageBinPath = packageBinPath;
|
|
7
|
+
exports.packPackage = packPackage;
|
|
8
|
+
exports.readJson = readJson;
|
|
9
|
+
exports.rehearseConsumerInstall = rehearseConsumerInstall;
|
|
10
|
+
exports.replaceConfigPlaceholders = replaceConfigPlaceholders;
|
|
11
|
+
exports.run = run;
|
|
12
|
+
exports.runExpectFailure = runExpectFailure;
|
|
13
|
+
exports.writeFakeArgent = writeFakeArgent;
|
|
14
|
+
exports.writeExistingAppFixture = writeExistingAppFixture;
|
|
15
|
+
exports.writeProfileEventFixtures = writeProfileEventFixtures;
|
|
16
|
+
exports.writeJson = writeJson;
|
|
17
|
+
const assert = require('node:assert/strict');
|
|
18
|
+
const fs = require('node:fs');
|
|
19
|
+
const os = require('node:os');
|
|
20
|
+
const path = require('node:path');
|
|
21
|
+
const { execFileSync } = require('node:child_process');
|
|
22
|
+
const DEFAULT_COMMAND_TIMEOUT_MS = 180_000;
|
|
23
|
+
/**
|
|
24
|
+
* Creates a clean npm environment for local tarball install rehearsals.
|
|
25
|
+
*
|
|
26
|
+
* @param {string} tempRoot
|
|
27
|
+
* @returns {NodeJS.ProcessEnv}
|
|
28
|
+
*/
|
|
29
|
+
function createRehearsalEnv(tempRoot) {
|
|
30
|
+
const env = { ...process.env };
|
|
31
|
+
for (const key of Object.keys(env)) {
|
|
32
|
+
if (key.toLowerCase().startsWith('npm_config_')) {
|
|
33
|
+
delete env[key];
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
env.npm_config_audit = 'false';
|
|
37
|
+
env.npm_config_cache = path.join(tempRoot, 'npm-cache');
|
|
38
|
+
env.npm_config_fund = 'false';
|
|
39
|
+
env.npm_config_update_notifier = 'false';
|
|
40
|
+
return env;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Resolves the per-command timeout for package gate child processes.
|
|
44
|
+
*
|
|
45
|
+
* @param {NodeJS.ProcessEnv} env
|
|
46
|
+
* @returns {number}
|
|
47
|
+
*/
|
|
48
|
+
function resolveCommandTimeoutMs(env) {
|
|
49
|
+
const timeoutMs = Number.parseInt(env.ASL_PACKAGE_GATE_TIMEOUT_MS ?? '', 10);
|
|
50
|
+
return Number.isFinite(timeoutMs) && timeoutMs > 0
|
|
51
|
+
? timeoutMs
|
|
52
|
+
: DEFAULT_COMMAND_TIMEOUT_MS;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Runs a command and returns stdout while preserving child stderr on failure.
|
|
56
|
+
*
|
|
57
|
+
* @param {string} command
|
|
58
|
+
* @param {string[]} args
|
|
59
|
+
* @param {RunOptions} options
|
|
60
|
+
* @returns {string}
|
|
61
|
+
*/
|
|
62
|
+
function run(command, args, options) {
|
|
63
|
+
try {
|
|
64
|
+
return execFileSync(command, args, {
|
|
65
|
+
cwd: options.cwd,
|
|
66
|
+
encoding: 'utf8',
|
|
67
|
+
env: options.env,
|
|
68
|
+
stdio: ['ignore', 'pipe', 'inherit'],
|
|
69
|
+
timeout: resolveCommandTimeoutMs(options.env),
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
const failed = error;
|
|
74
|
+
const stdout = Buffer.isBuffer(failed.stdout) ? failed.stdout.toString('utf8') : String(failed.stdout ?? '');
|
|
75
|
+
const status = failed.signal === 'SIGTERM' ? 'timeout' : failed.status ?? 'unknown';
|
|
76
|
+
throw new Error(`${command} ${args.join(' ')} failed with status ${status}\n${stdout}`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Runs a command that must fail and returns captured output.
|
|
81
|
+
*
|
|
82
|
+
* @param {string} command
|
|
83
|
+
* @param {string[]} args
|
|
84
|
+
* @param {RunOptions} options
|
|
85
|
+
* @returns {FailedRunOutput}
|
|
86
|
+
*/
|
|
87
|
+
function runExpectFailure(command, args, options) {
|
|
88
|
+
try {
|
|
89
|
+
const stdout = execFileSync(command, args, {
|
|
90
|
+
cwd: options.cwd,
|
|
91
|
+
encoding: 'utf8',
|
|
92
|
+
env: options.env,
|
|
93
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
94
|
+
timeout: resolveCommandTimeoutMs(options.env),
|
|
95
|
+
});
|
|
96
|
+
throw new Error(`Expected command to fail, but it passed with stdout: ${stdout}`);
|
|
97
|
+
}
|
|
98
|
+
catch (error) {
|
|
99
|
+
const failed = error;
|
|
100
|
+
if (failed.message.startsWith('Expected command to fail')) {
|
|
101
|
+
throw failed;
|
|
102
|
+
}
|
|
103
|
+
return {
|
|
104
|
+
status: failed.status ?? null,
|
|
105
|
+
stderr: Buffer.isBuffer(failed.stderr) ? failed.stderr.toString('utf8') : String(failed.stderr ?? ''),
|
|
106
|
+
stdout: Buffer.isBuffer(failed.stdout) ? failed.stdout.toString('utf8') : String(failed.stdout ?? ''),
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Reads and parses a JSON file.
|
|
112
|
+
*
|
|
113
|
+
* @param {string} filePath
|
|
114
|
+
* @returns {Record<string, unknown>}
|
|
115
|
+
*/
|
|
116
|
+
function readJson(filePath) {
|
|
117
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Writes stable formatted JSON.
|
|
121
|
+
*
|
|
122
|
+
* @param {string} filePath
|
|
123
|
+
* @param {unknown} value
|
|
124
|
+
* @returns {void}
|
|
125
|
+
*/
|
|
126
|
+
function writeJson(filePath, value) {
|
|
127
|
+
fs.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`, 'utf8');
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Resolves a package binary path inside a temporary npm app.
|
|
131
|
+
*
|
|
132
|
+
* @param {string} appRoot
|
|
133
|
+
* @param {string} name
|
|
134
|
+
* @returns {string}
|
|
135
|
+
*/
|
|
136
|
+
function packageBinPath(appRoot, name) {
|
|
137
|
+
const suffix = process.platform === 'win32' ? '.cmd' : '';
|
|
138
|
+
return path.join(appRoot, 'node_modules', '.bin', `${name}${suffix}`);
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Packs the current repo and returns the tarball path.
|
|
142
|
+
*
|
|
143
|
+
* @param {{env: NodeJS.ProcessEnv, packageRoot: string, packDir: string}} options
|
|
144
|
+
* @returns {string}
|
|
145
|
+
*/
|
|
146
|
+
function packPackage({ env, packageRoot, packDir, }) {
|
|
147
|
+
fs.mkdirSync(packDir, { recursive: true });
|
|
148
|
+
const packOutput = run('npm', ['pack', '--pack-destination', packDir], {
|
|
149
|
+
cwd: packageRoot,
|
|
150
|
+
env,
|
|
151
|
+
});
|
|
152
|
+
const tarballName = packOutput.trim().split(/\n/u).pop();
|
|
153
|
+
assert.ok(tarballName, 'npm pack did not print a tarball name');
|
|
154
|
+
const tarballPath = path.join(packDir, tarballName);
|
|
155
|
+
assert.equal(fs.existsSync(tarballPath), true, `missing packed tarball: ${tarballPath}`);
|
|
156
|
+
return tarballPath;
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Creates an existing app layout before Agent Scenario Loop initialization.
|
|
160
|
+
*
|
|
161
|
+
* @param {string} appRoot
|
|
162
|
+
* @returns {void}
|
|
163
|
+
*/
|
|
164
|
+
function writeExistingAppFixture(appRoot) {
|
|
165
|
+
fs.mkdirSync(path.join(appRoot, 'src'), { recursive: true });
|
|
166
|
+
writeJson(path.join(appRoot, 'package.json'), {
|
|
167
|
+
name: 'consumer-rehearsal-app',
|
|
168
|
+
private: true,
|
|
169
|
+
scripts: {
|
|
170
|
+
start: 'react-native start',
|
|
171
|
+
test: 'node --version',
|
|
172
|
+
},
|
|
173
|
+
});
|
|
174
|
+
fs.writeFileSync(path.join(appRoot, 'src', 'App.tsx'), [
|
|
175
|
+
"export function App() {",
|
|
176
|
+
" return null;",
|
|
177
|
+
"}",
|
|
178
|
+
'',
|
|
179
|
+
].join('\n'), 'utf8');
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Writes deterministic profile-event logs for the initialized consumer scenario.
|
|
183
|
+
*
|
|
184
|
+
* @param {string} appRoot
|
|
185
|
+
* @returns {{androidEvents: string, iosEvents: string}}
|
|
186
|
+
*/
|
|
187
|
+
function writeProfileEventFixtures(appRoot) {
|
|
188
|
+
const eventLogDir = path.join(appRoot, 'event-logs');
|
|
189
|
+
fs.mkdirSync(eventLogDir, { recursive: true });
|
|
190
|
+
const iosEvents = path.join(eventLogDir, 'account-overview-ios.log');
|
|
191
|
+
const androidEvents = path.join(eventLogDir, 'account-overview-android.log');
|
|
192
|
+
fs.writeFileSync(iosEvents, [
|
|
193
|
+
'2026-01-01T00:00:00.000Z consumer-ios [profile-event] {"event":"first_journey_started","scenario":"account-overview","runId":"account-overview-ios","iteration":1,"atMs":0}',
|
|
194
|
+
'2026-01-01T00:00:00.700Z consumer-ios [profile-event] {"event":"first_journey_completed","scenario":"account-overview","runId":"account-overview-ios","iteration":1,"atMs":700}',
|
|
195
|
+
'2026-01-01T00:00:01.000Z consumer-ios [profile-event] {"event":"first_journey_started","scenario":"account-overview","runId":"account-overview-ios","iteration":2,"atMs":1000}',
|
|
196
|
+
'2026-01-01T00:00:01.760Z consumer-ios [profile-event] {"event":"first_journey_completed","scenario":"account-overview","runId":"account-overview-ios","iteration":2,"atMs":1760}',
|
|
197
|
+
'2026-01-01T00:00:02.000Z consumer-ios [profile-event] {"event":"first_journey_started","scenario":"account-overview","runId":"account-overview-ios","iteration":3,"atMs":2000}',
|
|
198
|
+
'2026-01-01T00:00:02.830Z consumer-ios [profile-event] {"event":"first_journey_completed","scenario":"account-overview","runId":"account-overview-ios","iteration":3,"atMs":2830}',
|
|
199
|
+
'',
|
|
200
|
+
].join('\n'), 'utf8');
|
|
201
|
+
fs.writeFileSync(androidEvents, [
|
|
202
|
+
'2026-01-01T00:10:00.000Z consumer-android [profile-event] {"event":"first_journey_started","scenario":"account-overview","runId":"account-overview-android","iteration":1,"atMs":0}',
|
|
203
|
+
'2026-01-01T00:10:00.820Z consumer-android [profile-event] {"event":"first_journey_completed","scenario":"account-overview","runId":"account-overview-android","iteration":1,"atMs":820}',
|
|
204
|
+
'2026-01-01T00:10:01.000Z consumer-android [profile-event] {"event":"first_journey_started","scenario":"account-overview","runId":"account-overview-android","iteration":2,"atMs":1000}',
|
|
205
|
+
'2026-01-01T00:10:01.870Z consumer-android [profile-event] {"event":"first_journey_completed","scenario":"account-overview","runId":"account-overview-android","iteration":2,"atMs":1870}',
|
|
206
|
+
'2026-01-01T00:10:02.000Z consumer-android [profile-event] {"event":"first_journey_started","scenario":"account-overview","runId":"account-overview-android","iteration":3,"atMs":2000}',
|
|
207
|
+
'2026-01-01T00:10:02.940Z consumer-android [profile-event] {"event":"first_journey_completed","scenario":"account-overview","runId":"account-overview-android","iteration":3,"atMs":2940}',
|
|
208
|
+
'',
|
|
209
|
+
].join('\n'), 'utf8');
|
|
210
|
+
return { androidEvents, iosEvents };
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Writes a tiny Argent-compatible command for consumer-script rehearsal.
|
|
214
|
+
*
|
|
215
|
+
* @param {string} filePath
|
|
216
|
+
* @returns {void}
|
|
217
|
+
*/
|
|
218
|
+
function writeFakeArgent(filePath) {
|
|
219
|
+
const scriptPath = filePath.endsWith('.cmd') ? filePath.replace(/\.cmd$/u, '.js') : filePath;
|
|
220
|
+
const script = [
|
|
221
|
+
'#!/usr/bin/env node',
|
|
222
|
+
"const fs = require('node:fs');",
|
|
223
|
+
"const path = require('node:path');",
|
|
224
|
+
"const args = process.argv.slice(2);",
|
|
225
|
+
"const key = args.join(' ');",
|
|
226
|
+
"function ok(stdout = '{\"description\":\"Consumer Rehearsal first journey complete\"}\\n') { process.stdout.write(stdout); process.exit(0); }",
|
|
227
|
+
"if (args.includes('launch-app')) ok('{\"success\":true}\\n');",
|
|
228
|
+
"if (args.includes('describe')) ok();",
|
|
229
|
+
"if (args.includes('screenshot')) {",
|
|
230
|
+
" const screenshotPath = path.join(path.dirname(process.argv[1]), 'fake-argent-screenshot.png');",
|
|
231
|
+
" fs.writeFileSync(screenshotPath, 'fake screenshot', 'utf8');",
|
|
232
|
+
" ok(`Saved screenshot: ${screenshotPath}\\n`);",
|
|
233
|
+
"}",
|
|
234
|
+
"if (args.includes('gesture-tap')) ok('{\"success\":true}\\n');",
|
|
235
|
+
"if (args.includes('gesture-swipe')) ok('{\"success\":true}\\n');",
|
|
236
|
+
"process.stderr.write(`unexpected fake Argent command: ${key}\\n`);",
|
|
237
|
+
"process.exit(1);",
|
|
238
|
+
'',
|
|
239
|
+
].join('\n');
|
|
240
|
+
fs.writeFileSync(scriptPath, script, { mode: 0o755 });
|
|
241
|
+
if (filePath.endsWith('.cmd')) {
|
|
242
|
+
fs.writeFileSync(filePath, `@echo off\r\n"${process.execPath}" "%~dp0${path.basename(scriptPath)}" %*\r\n`, {
|
|
243
|
+
mode: 0o755,
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* Asserts one generated Argent package script can run from the installed app.
|
|
249
|
+
*
|
|
250
|
+
* @param {{appRoot: string, env: NodeJS.ProcessEnv, fakeArgentPath: string, platform: 'android' | 'ios'}} options
|
|
251
|
+
* @returns {void}
|
|
252
|
+
*/
|
|
253
|
+
function assertArgentScriptRuns({ appRoot, env, fakeArgentPath, platform, }) {
|
|
254
|
+
run('npm', ['run', `asl:argent:${platform}`, '--silent'], {
|
|
255
|
+
cwd: appRoot,
|
|
256
|
+
env: {
|
|
257
|
+
...env,
|
|
258
|
+
ASL_ARGENT_BIN: fakeArgentPath,
|
|
259
|
+
},
|
|
260
|
+
});
|
|
261
|
+
const runDir = path.join(appRoot, 'artifacts', 'asl', `argent-${platform}`);
|
|
262
|
+
const health = readJson(path.join(runDir, 'health.json'));
|
|
263
|
+
const verdict = readJson(path.join(runDir, 'verdict.json'));
|
|
264
|
+
const metadata = readJson(path.join(runDir, 'raw', 'argent-metadata.json'));
|
|
265
|
+
assert.equal(health.healthStatus, 'passed');
|
|
266
|
+
assert.equal(health.runId, `account-overview-${platform}-argent`);
|
|
267
|
+
assert.equal(health.scenarioId, 'account-overview');
|
|
268
|
+
assert.equal(verdict.runId, `account-overview-${platform}-argent`);
|
|
269
|
+
assert.equal(verdict.scenarioId, 'account-overview');
|
|
270
|
+
assert.equal(verdict.verdictStatus, 'not_evaluated');
|
|
271
|
+
assert.deepEqual(metadata.driverActions?.map((action) => action.driverAction), ['launch', 'tap', 'screenshot']);
|
|
272
|
+
assert.equal(fs.existsSync(path.join(runDir, 'captures', 'fake-argent-screenshot.png')), true);
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* Writes a small, valid platform live-proof artifact with all local pointers present.
|
|
276
|
+
*
|
|
277
|
+
* @param {{appRoot: string, platform: 'android' | 'ios'}} options
|
|
278
|
+
* @returns {string}
|
|
279
|
+
*/
|
|
280
|
+
function writeConsumerLiveProofFixture({ appRoot, platform, }) {
|
|
281
|
+
const scenarioId = 'account-overview';
|
|
282
|
+
const runId = `${platform}-live-proof-rehearsal`;
|
|
283
|
+
const platformRoot = path.join(appRoot, 'artifacts', 'asl', `${platform}-live`);
|
|
284
|
+
const liveProofDir = path.join(platformRoot, '_live-proof', `${platform}-live-proof`);
|
|
285
|
+
const preflightDir = path.join(platformRoot, 'preflight');
|
|
286
|
+
const profileDir = path.join(platformRoot, scenarioId, `${scenarioId}-${platform}-live`);
|
|
287
|
+
const interactionDir = path.join(platformRoot, 'interactions', `${scenarioId}-${platform}-agent-device`);
|
|
288
|
+
const comparisonBaselineDir = path.join(platformRoot, 'baselines', scenarioId);
|
|
289
|
+
const comparisonDir = path.join(platformRoot, 'comparisons', scenarioId, `${scenarioId}-${platform}-live`);
|
|
290
|
+
for (const dir of [
|
|
291
|
+
liveProofDir,
|
|
292
|
+
preflightDir,
|
|
293
|
+
profileDir,
|
|
294
|
+
interactionDir,
|
|
295
|
+
path.join(interactionDir, 'captures'),
|
|
296
|
+
comparisonBaselineDir,
|
|
297
|
+
comparisonDir,
|
|
298
|
+
]) {
|
|
299
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
300
|
+
}
|
|
301
|
+
for (const summaryPath of [
|
|
302
|
+
path.join(liveProofDir, 'agent-summary.md'),
|
|
303
|
+
path.join(preflightDir, 'agent-summary.md'),
|
|
304
|
+
path.join(profileDir, 'agent-summary.md'),
|
|
305
|
+
path.join(interactionDir, 'agent-summary.md'),
|
|
306
|
+
path.join(comparisonDir, 'agent-summary.md'),
|
|
307
|
+
]) {
|
|
308
|
+
fs.writeFileSync(summaryPath, `# ${platform} ${scenarioId} rehearsal\n`, 'utf8');
|
|
309
|
+
}
|
|
310
|
+
fs.writeFileSync(path.join(interactionDir, 'captures', 'final.png'), 'fake screenshot', 'utf8');
|
|
311
|
+
const liveProofPath = path.join(liveProofDir, 'live-proof.json');
|
|
312
|
+
writeJson(liveProofPath, {
|
|
313
|
+
schemaVersion: '1.0.0',
|
|
314
|
+
platform,
|
|
315
|
+
runId,
|
|
316
|
+
status: 'passed',
|
|
317
|
+
outputDir: path.relative(appRoot, liveProofDir),
|
|
318
|
+
preflight: {
|
|
319
|
+
runId: `${platform}-preflight`,
|
|
320
|
+
runDir: path.relative(appRoot, preflightDir),
|
|
321
|
+
summaryPath: path.relative(appRoot, path.join(preflightDir, 'agent-summary.md')),
|
|
322
|
+
healthStatus: 'passed',
|
|
323
|
+
verdictStatus: 'passed',
|
|
324
|
+
},
|
|
325
|
+
profiles: [
|
|
326
|
+
{
|
|
327
|
+
label: 'startup',
|
|
328
|
+
scenarioId,
|
|
329
|
+
runId: `${scenarioId}-${platform}-live`,
|
|
330
|
+
runDir: path.relative(appRoot, profileDir),
|
|
331
|
+
summaryPath: path.relative(appRoot, path.join(profileDir, 'agent-summary.md')),
|
|
332
|
+
healthStatus: 'passed',
|
|
333
|
+
verdictStatus: 'passed',
|
|
334
|
+
},
|
|
335
|
+
],
|
|
336
|
+
interactionProofs: [
|
|
337
|
+
{
|
|
338
|
+
label: 'agent-device startup',
|
|
339
|
+
runnerId: 'agent-device',
|
|
340
|
+
scenarioId,
|
|
341
|
+
runId: `${scenarioId}-${platform}-agent-device`,
|
|
342
|
+
runDir: path.relative(appRoot, interactionDir),
|
|
343
|
+
summaryPath: path.relative(appRoot, path.join(interactionDir, 'agent-summary.md')),
|
|
344
|
+
healthStatus: 'passed',
|
|
345
|
+
verdictStatus: 'not_evaluated',
|
|
346
|
+
captures: {
|
|
347
|
+
screenshots: ['captures/final.png'],
|
|
348
|
+
},
|
|
349
|
+
},
|
|
350
|
+
],
|
|
351
|
+
comparisons: [
|
|
352
|
+
{
|
|
353
|
+
label: 'startup',
|
|
354
|
+
scenarioId,
|
|
355
|
+
runId: `${scenarioId}-${platform}-live`,
|
|
356
|
+
status: 'better',
|
|
357
|
+
baselineDir: path.relative(appRoot, comparisonBaselineDir),
|
|
358
|
+
comparisonDir: path.relative(appRoot, comparisonDir),
|
|
359
|
+
summaryPath: path.relative(appRoot, path.join(comparisonDir, 'agent-summary.md')),
|
|
360
|
+
reason: null,
|
|
361
|
+
metricSummary: {
|
|
362
|
+
counts: {
|
|
363
|
+
better: 1,
|
|
364
|
+
worse: 0,
|
|
365
|
+
unchanged: 0,
|
|
366
|
+
inconclusive: 0,
|
|
367
|
+
low_confidence: 0,
|
|
368
|
+
},
|
|
369
|
+
notableMetrics: [
|
|
370
|
+
{
|
|
371
|
+
name: 'durationMs.p50',
|
|
372
|
+
status: 'better',
|
|
373
|
+
unit: 'ms',
|
|
374
|
+
baseline: 900,
|
|
375
|
+
current: 800,
|
|
376
|
+
delta: -100,
|
|
377
|
+
},
|
|
378
|
+
],
|
|
379
|
+
},
|
|
380
|
+
},
|
|
381
|
+
],
|
|
382
|
+
comparisonCounts: {
|
|
383
|
+
better: 1,
|
|
384
|
+
worse: 0,
|
|
385
|
+
unchanged: 0,
|
|
386
|
+
mixed: 0,
|
|
387
|
+
inconclusive: 0,
|
|
388
|
+
low_confidence: 0,
|
|
389
|
+
skipped: 0,
|
|
390
|
+
},
|
|
391
|
+
comparisonStatus: 'improved',
|
|
392
|
+
nextAction: {
|
|
393
|
+
code: 'inspect_summary',
|
|
394
|
+
summary: 'Inspect the rehearsal proof summary.',
|
|
395
|
+
},
|
|
396
|
+
summary: `${platform} rehearsal live proof passed.`,
|
|
397
|
+
});
|
|
398
|
+
return path.relative(appRoot, liveProofPath);
|
|
399
|
+
}
|
|
400
|
+
/**
|
|
401
|
+
* Asserts the generated two-platform live-proof script works from an installed app.
|
|
402
|
+
*
|
|
403
|
+
* @param {{appRoot: string, env: NodeJS.ProcessEnv}} options
|
|
404
|
+
* @returns {void}
|
|
405
|
+
*/
|
|
406
|
+
function assertGeneratedLiveProofGateRuns({ appRoot, env, }) {
|
|
407
|
+
const androidLiveProof = writeConsumerLiveProofFixture({ appRoot, platform: 'android' });
|
|
408
|
+
const iosLiveProof = writeConsumerLiveProofFixture({ appRoot, platform: 'ios' });
|
|
409
|
+
run('npm', ['run', 'asl:live-proof:both', '--silent'], {
|
|
410
|
+
cwd: appRoot,
|
|
411
|
+
env: {
|
|
412
|
+
...env,
|
|
413
|
+
ASL_ANDROID_LIVE_PROOF: androidLiveProof,
|
|
414
|
+
ASL_IOS_LIVE_PROOF: iosLiveProof,
|
|
415
|
+
ASL_REQUIRE_LIVE_PROOF_ARTIFACTS: '1',
|
|
416
|
+
},
|
|
417
|
+
});
|
|
418
|
+
const proofSet = readJson(path.join(appRoot, 'artifacts', 'asl', 'live-proof-set', 'live-proof-set.json'));
|
|
419
|
+
const summary = fs.readFileSync(path.join(appRoot, 'artifacts', 'asl', 'live-proof-set', 'agent-summary.md'), 'utf8');
|
|
420
|
+
assert.equal(proofSet.status, 'passed');
|
|
421
|
+
assert.deepEqual(proofSet.requiredPlatforms, ['android', 'ios']);
|
|
422
|
+
assert.deepEqual(proofSet.presentPlatforms, ['android', 'ios']);
|
|
423
|
+
assert.equal(proofSet.proofCount, 2);
|
|
424
|
+
assert.equal(proofSet.nextAction.code, 'inspect_summary');
|
|
425
|
+
assert.match(summary, /Status: passed/u);
|
|
426
|
+
assert.match(summary, /Present platforms: android, ios/u);
|
|
427
|
+
assert.match(summary, /android android-live-proof-rehearsal: status=passed comparison=improved/u);
|
|
428
|
+
assert.match(summary, /ios ios-live-proof-rehearsal: status=passed comparison=improved/u);
|
|
429
|
+
}
|
|
430
|
+
/**
|
|
431
|
+
* Merges generated Agent Scenario Loop package-script snippets into the app package.json.
|
|
432
|
+
*
|
|
433
|
+
* @param {string} appRoot
|
|
434
|
+
* @returns {Record<string, string>}
|
|
435
|
+
*/
|
|
436
|
+
function mergeGeneratedScripts(appRoot) {
|
|
437
|
+
const packagePath = path.join(appRoot, 'package.json');
|
|
438
|
+
const packageJson = readJson(packagePath);
|
|
439
|
+
const generatedScripts = readJson(path.join(appRoot, 'asl', 'package-scripts.json'));
|
|
440
|
+
packageJson.scripts = {
|
|
441
|
+
...(packageJson.scripts ?? {}),
|
|
442
|
+
...generatedScripts,
|
|
443
|
+
};
|
|
444
|
+
writeJson(packagePath, packageJson);
|
|
445
|
+
return generatedScripts;
|
|
446
|
+
}
|
|
447
|
+
/**
|
|
448
|
+
* Merges the generated runtime-artifact ignore snippet into the fixture app.
|
|
449
|
+
*
|
|
450
|
+
* @param {string} appRoot
|
|
451
|
+
* @returns {void}
|
|
452
|
+
*/
|
|
453
|
+
function mergeGitignoreSnippet(appRoot) {
|
|
454
|
+
const gitignorePath = path.join(appRoot, '.gitignore');
|
|
455
|
+
const snippet = fs.readFileSync(path.join(appRoot, 'asl', 'gitignore-snippet'), 'utf8').trim();
|
|
456
|
+
const existing = fs.existsSync(gitignorePath) ? fs.readFileSync(gitignorePath, 'utf8').trim() : '';
|
|
457
|
+
fs.writeFileSync(gitignorePath, `${[existing, snippet].filter(Boolean).join('\n\n')}\n`, 'utf8');
|
|
458
|
+
}
|
|
459
|
+
/**
|
|
460
|
+
* Replaces scaffold placeholders with realistic app identifiers before validation.
|
|
461
|
+
*
|
|
462
|
+
* @param {string} appRoot
|
|
463
|
+
* @returns {void}
|
|
464
|
+
*/
|
|
465
|
+
function replaceConfigPlaceholders(appRoot) {
|
|
466
|
+
const configPath = path.join(appRoot, 'asl.config.json');
|
|
467
|
+
const config = readJson(configPath);
|
|
468
|
+
config.projectName = 'consumer-rehearsal';
|
|
469
|
+
config.app = {
|
|
470
|
+
...(config.app ?? {}),
|
|
471
|
+
displayName: 'Consumer Rehearsal',
|
|
472
|
+
scheme: 'consumer-rehearsal',
|
|
473
|
+
profileSessionScheme: 'consumer-rehearsal',
|
|
474
|
+
iosBundleId: 'dev.agent-scenario-loop.consumer-rehearsal',
|
|
475
|
+
androidPackage: 'dev.agentscenarioloop.consumerrehearsal',
|
|
476
|
+
ios: {
|
|
477
|
+
...((config.app ?? {}).ios ?? {}),
|
|
478
|
+
xcodeScheme: 'ConsumerRehearsal',
|
|
479
|
+
},
|
|
480
|
+
};
|
|
481
|
+
writeJson(configPath, config);
|
|
482
|
+
}
|
|
483
|
+
/**
|
|
484
|
+
* Asserts that installed project validation rejects stale merged package scripts.
|
|
485
|
+
*
|
|
486
|
+
* @param {{appRoot: string, env: NodeJS.ProcessEnv}} options
|
|
487
|
+
* @returns {void}
|
|
488
|
+
*/
|
|
489
|
+
function assertPackageScriptDriftFails({ appRoot, env, }) {
|
|
490
|
+
const packagePath = path.join(appRoot, 'package.json');
|
|
491
|
+
const packageJson = readJson(packagePath);
|
|
492
|
+
packageJson.scripts['asl:profile:android'] = [
|
|
493
|
+
'asl-profile-android',
|
|
494
|
+
'--config asl.config.json',
|
|
495
|
+
'--scenario scenarios/mobile/account-overview.json',
|
|
496
|
+
'--comparison-lane stale-android',
|
|
497
|
+
'--out artifacts/asl/android/stale',
|
|
498
|
+
'--run-id stale-android',
|
|
499
|
+
].join(' ');
|
|
500
|
+
packageJson.scripts['asl:android:live'] = [
|
|
501
|
+
'asl-live-android',
|
|
502
|
+
'--config asl.config.json',
|
|
503
|
+
'--scenario scenarios/mobile/account-overview.json',
|
|
504
|
+
'--out artifacts/asl/android-live',
|
|
505
|
+
].join(' ');
|
|
506
|
+
writeJson(packagePath, packageJson);
|
|
507
|
+
const outDir = path.join(appRoot, 'artifacts', 'asl', 'project-validation-drift');
|
|
508
|
+
const failure = runExpectFailure(packageBinPath(appRoot, 'asl-validate-project'), [
|
|
509
|
+
'--root',
|
|
510
|
+
appRoot,
|
|
511
|
+
'--platform',
|
|
512
|
+
'all',
|
|
513
|
+
'--out',
|
|
514
|
+
outDir,
|
|
515
|
+
], {
|
|
516
|
+
cwd: appRoot,
|
|
517
|
+
env,
|
|
518
|
+
});
|
|
519
|
+
assert.notEqual(failure.status, 0);
|
|
520
|
+
assert.match(failure.stdout, /project validation failed/u);
|
|
521
|
+
assert.match(failure.stdout, /App package\.json ASL script\(s\) differ/u);
|
|
522
|
+
const validation = readJson(path.join(outDir, 'project-validation.json'));
|
|
523
|
+
assert.equal(validation.status, 'failed');
|
|
524
|
+
assert.deepEqual(validation.scripts.mismatchedPackageJsonScripts, ['asl:profile:android', 'asl:android:live']);
|
|
525
|
+
assert.equal(validation.nextActions.some((action) => action.code === 'merge_package_scripts'), true);
|
|
526
|
+
}
|
|
527
|
+
/**
|
|
528
|
+
* Asserts that installed project validation rejects invalid path config.
|
|
529
|
+
*
|
|
530
|
+
* @param {{appRoot: string, env: NodeJS.ProcessEnv}} options
|
|
531
|
+
* @returns {void}
|
|
532
|
+
*/
|
|
533
|
+
function assertProjectPathConfigFails({ appRoot, env, }) {
|
|
534
|
+
const configPath = path.join(appRoot, 'asl.config.json');
|
|
535
|
+
const originalConfig = readJson(configPath);
|
|
536
|
+
const brokenConfig = JSON.parse(JSON.stringify(originalConfig));
|
|
537
|
+
brokenConfig.paths = {
|
|
538
|
+
...(brokenConfig.paths ?? {}),
|
|
539
|
+
androidArtifactsRoot: 42,
|
|
540
|
+
};
|
|
541
|
+
delete brokenConfig.paths.scenarioRoot;
|
|
542
|
+
delete brokenConfig.paths.iosScenarioRoot;
|
|
543
|
+
delete brokenConfig.paths.androidScenarioRoot;
|
|
544
|
+
writeJson(configPath, brokenConfig);
|
|
545
|
+
try {
|
|
546
|
+
const outDir = path.join(appRoot, 'artifacts', 'asl', 'project-validation-path-config');
|
|
547
|
+
const failure = runExpectFailure(packageBinPath(appRoot, 'asl-validate-project'), [
|
|
548
|
+
'--root',
|
|
549
|
+
appRoot,
|
|
550
|
+
'--platform',
|
|
551
|
+
'all',
|
|
552
|
+
'--out',
|
|
553
|
+
outDir,
|
|
554
|
+
], {
|
|
555
|
+
cwd: appRoot,
|
|
556
|
+
env,
|
|
557
|
+
});
|
|
558
|
+
assert.notEqual(failure.status, 0);
|
|
559
|
+
assert.match(failure.stdout, /project validation failed/u);
|
|
560
|
+
assert.match(failure.stdout, /Project config is missing required field\(s\)/u);
|
|
561
|
+
assert.match(failure.stdout, /Project config has invalid required field\(s\)/u);
|
|
562
|
+
const validation = readJson(path.join(outDir, 'project-validation.json'));
|
|
563
|
+
assert.equal(validation.status, 'failed');
|
|
564
|
+
assert.equal(validation.config.status, 'incomplete');
|
|
565
|
+
assert.deepEqual(validation.config.missingFields, [
|
|
566
|
+
'paths.scenarioRoot or paths.iosScenarioRoot',
|
|
567
|
+
'paths.scenarioRoot or paths.androidScenarioRoot',
|
|
568
|
+
]);
|
|
569
|
+
assert.deepEqual(validation.config.invalidFields, ['paths.androidArtifactsRoot']);
|
|
570
|
+
assert.equal(validation.nextActions.some((action) => action.code === 'fix_project_config'), true);
|
|
571
|
+
}
|
|
572
|
+
finally {
|
|
573
|
+
writeJson(configPath, originalConfig);
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
/**
|
|
577
|
+
* Asserts that the installed package can initialize and validate an existing app.
|
|
578
|
+
*
|
|
579
|
+
* @param {{appRoot: string, env: NodeJS.ProcessEnv, tarballPath: string}} options
|
|
580
|
+
* @returns {void}
|
|
581
|
+
*/
|
|
582
|
+
function rehearseConsumerInstall({ appRoot, env, tarballPath, }) {
|
|
583
|
+
writeExistingAppFixture(appRoot);
|
|
584
|
+
run('npm', ['install', tarballPath, '--ignore-scripts'], {
|
|
585
|
+
cwd: appRoot,
|
|
586
|
+
env,
|
|
587
|
+
});
|
|
588
|
+
run(packageBinPath(appRoot, 'asl-init'), ['--out', appRoot, '--scenario', 'Account Overview'], {
|
|
589
|
+
cwd: appRoot,
|
|
590
|
+
env,
|
|
591
|
+
});
|
|
592
|
+
const generatedScripts = mergeGeneratedScripts(appRoot);
|
|
593
|
+
mergeGitignoreSnippet(appRoot);
|
|
594
|
+
replaceConfigPlaceholders(appRoot);
|
|
595
|
+
const profileEvents = writeProfileEventFixtures(appRoot);
|
|
596
|
+
const fakeArgentPath = path.join(appRoot, process.platform === 'win32' ? 'fake-argent.cmd' : 'fake-argent');
|
|
597
|
+
writeFakeArgent(fakeArgentPath);
|
|
598
|
+
const packageJson = readJson(path.join(appRoot, 'package.json'));
|
|
599
|
+
assert.equal(packageJson.scripts.start, 'react-native start');
|
|
600
|
+
assert.equal(packageJson.scripts.test, 'node --version');
|
|
601
|
+
for (const scriptName of Object.keys(generatedScripts)) {
|
|
602
|
+
assert.equal(typeof packageJson.scripts[scriptName], 'string', `${scriptName} should be merged`);
|
|
603
|
+
}
|
|
604
|
+
run('npm', ['run', 'asl:check:ios', '--silent'], {
|
|
605
|
+
cwd: appRoot,
|
|
606
|
+
env,
|
|
607
|
+
});
|
|
608
|
+
run('npm', ['run', 'asl:check:android', '--silent'], {
|
|
609
|
+
cwd: appRoot,
|
|
610
|
+
env,
|
|
611
|
+
});
|
|
612
|
+
run('npm', ['run', 'asl:profile:ios', '--silent'], {
|
|
613
|
+
cwd: appRoot,
|
|
614
|
+
env: { ...env, ASL_PROFILE_IOS_EVENTS: profileEvents.iosEvents },
|
|
615
|
+
});
|
|
616
|
+
run('npm', ['run', 'asl:profile:android', '--silent'], {
|
|
617
|
+
cwd: appRoot,
|
|
618
|
+
env: { ...env, ASL_PROFILE_ANDROID_EVENTS: profileEvents.androidEvents },
|
|
619
|
+
});
|
|
620
|
+
run('npm', ['run', 'asl:profile:ios:provider', '--silent'], {
|
|
621
|
+
cwd: appRoot,
|
|
622
|
+
env: { ...env, ASL_PROFILE_IOS_EVENTS: profileEvents.iosEvents },
|
|
623
|
+
});
|
|
624
|
+
run('npm', ['run', 'asl:profile:android:provider', '--silent'], {
|
|
625
|
+
cwd: appRoot,
|
|
626
|
+
env: { ...env, ASL_PROFILE_ANDROID_EVENTS: profileEvents.androidEvents },
|
|
627
|
+
});
|
|
628
|
+
assertArgentScriptRuns({ appRoot, env, fakeArgentPath, platform: 'ios' });
|
|
629
|
+
assertArgentScriptRuns({ appRoot, env, fakeArgentPath, platform: 'android' });
|
|
630
|
+
assertGeneratedLiveProofGateRuns({ appRoot, env });
|
|
631
|
+
run('npm', ['run', 'asl:validate', '--silent'], {
|
|
632
|
+
cwd: appRoot,
|
|
633
|
+
env,
|
|
634
|
+
});
|
|
635
|
+
const validationPath = path.join(appRoot, 'artifacts', 'asl', 'project-validation', 'project-validation.json');
|
|
636
|
+
const validation = readJson(validationPath);
|
|
637
|
+
assert.equal(validation.status, 'passed');
|
|
638
|
+
assert.equal(validation.appHelper.status, 'present');
|
|
639
|
+
assert.deepEqual(validation.config.customDrivers, []);
|
|
640
|
+
assert.deepEqual(validation.config.externalTargetDrivers, ['xcodebuildmcp']);
|
|
641
|
+
assert.deepEqual(validation.config.packageSupportedDrivers, ['adb', 'agent-device', 'argent', 'fixture-log-ingest', 'ios-simctl']);
|
|
642
|
+
assert.deepEqual(validation.config.supportedDrivers, ['adb', 'agent-device', 'argent', 'fixture-log-ingest', 'ios-simctl', 'xcodebuildmcp']);
|
|
643
|
+
assert.deepEqual(validation.config.missingSupportedDrivers, []);
|
|
644
|
+
const realAppRoot = fs.realpathSync(appRoot);
|
|
645
|
+
assert.deepEqual(validation.scenarioCandidateDirectories.map((directory) => path.relative(realAppRoot, fs.realpathSync(directory))), [
|
|
646
|
+
'scenarios',
|
|
647
|
+
path.join('scenarios', 'mobile'),
|
|
648
|
+
]);
|
|
649
|
+
assert.equal(validation.scripts.status, 'present');
|
|
650
|
+
assert.equal(validation.scripts.packageJsonStatus, 'present');
|
|
651
|
+
assert.deepEqual(validation.warnings, []);
|
|
652
|
+
assert.equal(validation.nextActions.some((action) => action.code === 'replace_config_placeholders'), false);
|
|
653
|
+
assert.deepEqual(validation.plans
|
|
654
|
+
.map((plan) => ({
|
|
655
|
+
healthStatus: plan.healthStatus,
|
|
656
|
+
platform: plan.platform,
|
|
657
|
+
scenarioId: plan.scenarioId,
|
|
658
|
+
}))
|
|
659
|
+
.sort((left, right) => left.platform.localeCompare(right.platform)), [
|
|
660
|
+
{ healthStatus: 'passed', platform: 'android', scenarioId: 'account-overview' },
|
|
661
|
+
{ healthStatus: 'passed', platform: 'ios', scenarioId: 'account-overview' },
|
|
662
|
+
]);
|
|
663
|
+
for (const [platform, runId] of [['ios', 'account-overview-ios'], ['android', 'account-overview-android']]) {
|
|
664
|
+
const runDir = path.join(appRoot, 'artifacts', 'asl', platform, 'account-overview', runId);
|
|
665
|
+
const health = readJson(path.join(runDir, 'health.json'));
|
|
666
|
+
const verdict = readJson(path.join(runDir, 'verdict.json'));
|
|
667
|
+
assert.equal(health.healthStatus, 'passed');
|
|
668
|
+
assert.equal(verdict.verdictStatus, 'passed');
|
|
669
|
+
}
|
|
670
|
+
for (const [platform, runId] of [
|
|
671
|
+
['ios', 'account-overview-ios-provider'],
|
|
672
|
+
['android', 'account-overview-android-provider'],
|
|
673
|
+
]) {
|
|
674
|
+
const providerRunDir = path.join(appRoot, 'artifacts', 'asl', platform, 'account-overview', runId);
|
|
675
|
+
const providerManifest = readJson(path.join(providerRunDir, 'manifest.json'));
|
|
676
|
+
assert.deepEqual(providerManifest.artifacts?.signals?.js, ['signals/js/profiler.json']);
|
|
677
|
+
assert.deepEqual(providerManifest.artifacts?.signals?.memory, ['signals/memory/memory.json']);
|
|
678
|
+
assert.deepEqual(providerManifest.artifacts?.signals?.network, ['signals/network/network.har']);
|
|
679
|
+
assert.deepEqual(providerManifest.artifacts?.evidenceAttachments?.map((attachment) => ({
|
|
680
|
+
channel: attachment.channel,
|
|
681
|
+
kind: attachment.kind,
|
|
682
|
+
path: attachment.path,
|
|
683
|
+
sourceFileName: attachment.sourceFileName,
|
|
684
|
+
})), [
|
|
685
|
+
{
|
|
686
|
+
channel: 'provider',
|
|
687
|
+
kind: 'accessibility',
|
|
688
|
+
path: 'raw/providers/example-evidence-provider/accessibility.json',
|
|
689
|
+
sourceFileName: 'accessibility.json',
|
|
690
|
+
},
|
|
691
|
+
{
|
|
692
|
+
channel: 'provider',
|
|
693
|
+
kind: 'profiler',
|
|
694
|
+
path: 'raw/providers/example-evidence-provider/profiler.json',
|
|
695
|
+
sourceFileName: 'profiler.json',
|
|
696
|
+
},
|
|
697
|
+
{
|
|
698
|
+
channel: 'signal',
|
|
699
|
+
kind: 'js',
|
|
700
|
+
path: 'signals/js/profiler.json',
|
|
701
|
+
sourceFileName: 'profiler.json',
|
|
702
|
+
},
|
|
703
|
+
{
|
|
704
|
+
channel: 'signal',
|
|
705
|
+
kind: 'memory',
|
|
706
|
+
path: 'signals/memory/memory.json',
|
|
707
|
+
sourceFileName: 'memory.json',
|
|
708
|
+
},
|
|
709
|
+
{
|
|
710
|
+
channel: 'signal',
|
|
711
|
+
kind: 'network',
|
|
712
|
+
path: 'signals/network/network.har',
|
|
713
|
+
sourceFileName: 'network.har',
|
|
714
|
+
},
|
|
715
|
+
]);
|
|
716
|
+
}
|
|
717
|
+
assertProjectPathConfigFails({ appRoot, env });
|
|
718
|
+
assertPackageScriptDriftFails({ appRoot, env });
|
|
719
|
+
}
|
|
720
|
+
/**
|
|
721
|
+
* Runs the packed-package consumer rehearsal.
|
|
722
|
+
*
|
|
723
|
+
* @returns {void}
|
|
724
|
+
*/
|
|
725
|
+
function main() {
|
|
726
|
+
const packageRoot = path.resolve(__dirname, '..', '..');
|
|
727
|
+
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'asl-consumer-rehearsal-'));
|
|
728
|
+
const env = createRehearsalEnv(tempRoot);
|
|
729
|
+
const appRoot = path.join(tempRoot, 'existing-mobile-app');
|
|
730
|
+
fs.mkdirSync(appRoot, { recursive: true });
|
|
731
|
+
try {
|
|
732
|
+
const tarballPath = packPackage({
|
|
733
|
+
env,
|
|
734
|
+
packageRoot,
|
|
735
|
+
packDir: path.join(tempRoot, 'pack'),
|
|
736
|
+
});
|
|
737
|
+
rehearseConsumerInstall({
|
|
738
|
+
appRoot,
|
|
739
|
+
env,
|
|
740
|
+
tarballPath,
|
|
741
|
+
});
|
|
742
|
+
process.stdout.write(`consumer rehearsal passed: ${appRoot}\n`);
|
|
743
|
+
}
|
|
744
|
+
catch (error) {
|
|
745
|
+
console.error(`consumer rehearsal temp kept at: ${tempRoot}`);
|
|
746
|
+
throw error;
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
if (require.main === module) {
|
|
750
|
+
try {
|
|
751
|
+
main();
|
|
752
|
+
}
|
|
753
|
+
catch (error) {
|
|
754
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
755
|
+
process.exitCode = 1;
|
|
756
|
+
}
|
|
757
|
+
}
|