agent-qa 0.1.14 → 0.1.16
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/README.md +1 -1
- package/dist/cli.cjs +504 -126
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +475 -88
- package/dist/cli.js.map +1 -1
- package/package.json +10 -9
package/dist/cli.js
CHANGED
|
@@ -337,7 +337,7 @@ var init_llm_utils = __esm({
|
|
|
337
337
|
});
|
|
338
338
|
|
|
339
339
|
// src/cli.ts
|
|
340
|
-
import { Command as
|
|
340
|
+
import { Command as Command20 } from "commander";
|
|
341
341
|
|
|
342
342
|
// src/index.ts
|
|
343
343
|
var index_exports = {};
|
|
@@ -501,10 +501,12 @@ import select from "@inquirer/select";
|
|
|
501
501
|
import input from "@inquirer/input";
|
|
502
502
|
import checkbox from "@inquirer/checkbox";
|
|
503
503
|
import {
|
|
504
|
+
DEFAULT_AGENT_QA_AUTH_STATES_DIR,
|
|
504
505
|
DEFAULT_AGENT_QA_CACHE_DIR as DEFAULT_AGENT_QA_CACHE_DIR2,
|
|
505
506
|
DEFAULT_AGENT_QA_RUNTIME_DIR
|
|
506
507
|
} from "@vostride/agent-qa-core";
|
|
507
508
|
var INIT_CACHE_DIR = DEFAULT_AGENT_QA_CACHE_DIR2 || ".agent-qa/cache";
|
|
509
|
+
var INIT_AUTH_STATES_DIR = DEFAULT_AGENT_QA_AUTH_STATES_DIR || ".agent-qa/auth-states";
|
|
508
510
|
var INIT_RUNTIME_DIR = DEFAULT_AGENT_QA_RUNTIME_DIR || ".agent-qa";
|
|
509
511
|
var SUBSCRIPTION_AUTH_PACKAGE = "@vostride/agent-qa-subscription-auth";
|
|
510
512
|
var PACKAGE_JSON_FILE = "package.json";
|
|
@@ -837,6 +839,7 @@ function buildDefaultConfig(platform, providerOrLLMs, model, baseURL) {
|
|
|
837
839
|
dashboard: { port: 3100, artifactsDir: ".agent-qa/artifacts" },
|
|
838
840
|
mcp: { enabled: true, transport: "http", host: "127.0.0.1", port: 3471, path: "/mcp" },
|
|
839
841
|
cache: { dir: INIT_CACHE_DIR, ttl: "7d" },
|
|
842
|
+
authState: { dir: INIT_AUTH_STATES_DIR },
|
|
840
843
|
...hasWeb(platform) ? {
|
|
841
844
|
accessibility: {
|
|
842
845
|
enabled: true,
|
|
@@ -878,7 +881,7 @@ function addYamlComments(yamlStr) {
|
|
|
878
881
|
if (line.startsWith("workspace:")) {
|
|
879
882
|
result += "# File discovery and project settings\n";
|
|
880
883
|
} else if (line.startsWith("services:")) {
|
|
881
|
-
result += "\n# Infrastructure services (dashboard, MCP, cache, logging)\n";
|
|
884
|
+
result += "\n# Infrastructure services (dashboard, MCP, cache, auth state, logging)\n";
|
|
882
885
|
} else if (line.startsWith("registry:")) {
|
|
883
886
|
result += "\n# Named resource definitions (LLM configs, app targets)\n";
|
|
884
887
|
} else if (line.startsWith("plugins:")) {
|
|
@@ -933,7 +936,19 @@ var DEFAULT_AGENT_RULES_FILE = "agent-rules.md";
|
|
|
933
936
|
var DEFAULT_ENV_FILE = ".env";
|
|
934
937
|
var DEFAULT_SECRETS_FILE = ".env.secrets.local";
|
|
935
938
|
var DEFAULT_LOCAL_CONFIG_FILE = "agent-qa.local.yaml";
|
|
936
|
-
|
|
939
|
+
function directoryGitignoreEntry(value) {
|
|
940
|
+
return value.endsWith("/") ? value : `${value}/`;
|
|
941
|
+
}
|
|
942
|
+
function buildGitignoreEntries() {
|
|
943
|
+
return [
|
|
944
|
+
directoryGitignoreEntry(INIT_RUNTIME_DIR),
|
|
945
|
+
directoryGitignoreEntry(INIT_AUTH_STATES_DIR),
|
|
946
|
+
"node_modules/",
|
|
947
|
+
DEFAULT_LOCAL_CONFIG_FILE,
|
|
948
|
+
DEFAULT_ENV_FILE,
|
|
949
|
+
DEFAULT_SECRETS_FILE
|
|
950
|
+
];
|
|
951
|
+
}
|
|
937
952
|
function buildLocalConfigTemplate() {
|
|
938
953
|
return `# This file is for machine-specific device, app, and provider bindings.
|
|
939
954
|
# Keep it out of git.
|
|
@@ -962,7 +977,10 @@ function appendGitignore(dir) {
|
|
|
962
977
|
if (existsSync(gitignorePath)) {
|
|
963
978
|
existing = readFileSync(gitignorePath, "utf-8");
|
|
964
979
|
}
|
|
965
|
-
const
|
|
980
|
+
const existingEntries = new Set(
|
|
981
|
+
existing.split(/\r?\n/).map((line) => line.trim()).filter(Boolean)
|
|
982
|
+
);
|
|
983
|
+
const missing = buildGitignoreEntries().filter((entry) => !existingEntries.has(entry));
|
|
966
984
|
if (missing.length === 0) return;
|
|
967
985
|
const append = (existing.length > 0 && !existing.endsWith("\n") ? "\n" : "") + missing.join("\n") + "\n";
|
|
968
986
|
writeFileSync(gitignorePath, existing + append);
|
|
@@ -2096,7 +2114,7 @@ import { randomUUID } from "crypto";
|
|
|
2096
2114
|
import { resolve as resolvePath3 } from "path";
|
|
2097
2115
|
import { Command as Command6 } from "commander";
|
|
2098
2116
|
import pc7 from "picocolors";
|
|
2099
|
-
import { ATTR_RUNNER, ATTR_TRIGGER, buildInternalRunAttributes, DEFAULT_AGENT_QA_ARTIFACTS_DIR, DEFAULT_AGENT_QA_CACHE_DIR as DEFAULT_AGENT_QA_CACHE_DIR3, DEFAULT_AGENT_QA_VIDEOS_DIR, formatRunAttributesBlock, generateRunId, mergeRunAttributes, MobileSetupError, parseRunAttrFlags, resolveMemoryRoot, resolveMobileRunConfig, validateTrustedRunAttributes } from "@vostride/agent-qa-core";
|
|
2117
|
+
import { ATTR_RUNNER, ATTR_TRIGGER, AUTH_STATE_SCHEMA_VERSION, buildInternalRunAttributes, DEFAULT_AGENT_QA_ARTIFACTS_DIR, DEFAULT_AGENT_QA_CACHE_DIR as DEFAULT_AGENT_QA_CACHE_DIR3, DEFAULT_AGENT_QA_VIDEOS_DIR, formatRunAttributesBlock, generateRunId, mergeRunAttributes, MobileSetupError, normalizeAuthStateUse, parseRunAttrFlags, redactAuthStateValue, resolveAuthStateForRun, resolveAuthStatePaths, resolveMemoryRoot, resolveMobileRunConfig, validateTrustedRunAttributes } from "@vostride/agent-qa-core";
|
|
2100
2118
|
import { discoverWorkspaceFiles as discoverWorkspaceFiles2, isWorkspacePathMatch, resolveWorkspaceFileTarget, resolveWorkspacePaths as resolveWorkspacePaths2 } from "@vostride/agent-qa-core";
|
|
2101
2119
|
|
|
2102
2120
|
// src/devices.ts
|
|
@@ -2580,7 +2598,7 @@ async function loadRuntimeSecrets(input3) {
|
|
|
2580
2598
|
}
|
|
2581
2599
|
}
|
|
2582
2600
|
function buildRunArtifactContext(input3) {
|
|
2583
|
-
return {
|
|
2601
|
+
return redactAuthStateValue({
|
|
2584
2602
|
artifact: {
|
|
2585
2603
|
kind: input3.kind,
|
|
2586
2604
|
config: {
|
|
@@ -2614,7 +2632,7 @@ function buildRunArtifactContext(input3) {
|
|
|
2614
2632
|
attributes: input3.attributes
|
|
2615
2633
|
}
|
|
2616
2634
|
}
|
|
2617
|
-
};
|
|
2635
|
+
});
|
|
2618
2636
|
}
|
|
2619
2637
|
async function resolveDeviceAndFarmSession(config, deviceName, testPlatform, testName, testTimeout, preResolvedDevice, localBindingsOverride, mobileResolved) {
|
|
2620
2638
|
if (!deviceName || testPlatform === "web") return {};
|
|
@@ -2692,6 +2710,82 @@ function resolveMobileRunOrThrow(input3) {
|
|
|
2692
2710
|
wrapMobileSetupError(err);
|
|
2693
2711
|
}
|
|
2694
2712
|
}
|
|
2713
|
+
function getSelectedAuthStateUse(use) {
|
|
2714
|
+
return normalizeAuthStateUse(use);
|
|
2715
|
+
}
|
|
2716
|
+
async function resolveSelectedAuthStateForRun(input3) {
|
|
2717
|
+
if (!input3.authState || input3.authState.load === false) return void 0;
|
|
2718
|
+
try {
|
|
2719
|
+
return await resolveAuthStateForRun({
|
|
2720
|
+
configDir: input3.configDir,
|
|
2721
|
+
authStateDir: input3.config.services?.authState?.dir,
|
|
2722
|
+
targetName: input3.resolvedTarget.name,
|
|
2723
|
+
stateName: input3.authState.name,
|
|
2724
|
+
target: { platform: input3.resolvedTarget.platform }
|
|
2725
|
+
});
|
|
2726
|
+
} catch (err) {
|
|
2727
|
+
console.error(pc7.red(`Error: ${err instanceof Error ? err.message : String(err)}`));
|
|
2728
|
+
process.exit(2);
|
|
2729
|
+
}
|
|
2730
|
+
}
|
|
2731
|
+
function resolveAuthStateCapturePathsForRun(input3) {
|
|
2732
|
+
if (input3.authState?.capture !== true) return void 0;
|
|
2733
|
+
try {
|
|
2734
|
+
return resolveAuthStatePaths({
|
|
2735
|
+
configDir: input3.configDir,
|
|
2736
|
+
authStateDir: input3.config.services?.authState?.dir,
|
|
2737
|
+
targetName: input3.resolvedTarget.name,
|
|
2738
|
+
stateName: input3.authState.name,
|
|
2739
|
+
target: { platform: input3.resolvedTarget.platform }
|
|
2740
|
+
});
|
|
2741
|
+
} catch (err) {
|
|
2742
|
+
console.error(pc7.red(`Error: ${err instanceof Error ? err.message : String(err)}`));
|
|
2743
|
+
process.exit(2);
|
|
2744
|
+
}
|
|
2745
|
+
}
|
|
2746
|
+
function formatAuthStateCaptureFailure(paths) {
|
|
2747
|
+
return `Could not save auth state "${paths.stateName}" for target "${paths.targetName}".`;
|
|
2748
|
+
}
|
|
2749
|
+
function getStorageStateContext(adapter) {
|
|
2750
|
+
const maybeGetPage = adapter.getPage;
|
|
2751
|
+
if (typeof maybeGetPage !== "function") return null;
|
|
2752
|
+
const page = maybeGetPage.call(adapter);
|
|
2753
|
+
if (!page || typeof page !== "object") return null;
|
|
2754
|
+
const maybeContext = page.context;
|
|
2755
|
+
if (typeof maybeContext !== "function") return null;
|
|
2756
|
+
const context = maybeContext.call(page);
|
|
2757
|
+
if (!context || typeof context !== "object") return null;
|
|
2758
|
+
const storageState = context.storageState;
|
|
2759
|
+
if (typeof storageState !== "function") return null;
|
|
2760
|
+
return {
|
|
2761
|
+
storageState: storageState.bind(context)
|
|
2762
|
+
};
|
|
2763
|
+
}
|
|
2764
|
+
async function captureAuthStateForRun(input3) {
|
|
2765
|
+
const context = getStorageStateContext(input3.adapter);
|
|
2766
|
+
if (!context) {
|
|
2767
|
+
throw new Error("Web auth-state capture requires a Playwright browser context.");
|
|
2768
|
+
}
|
|
2769
|
+
const payload = await context.storageState({ indexedDB: true });
|
|
2770
|
+
const capturedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2771
|
+
const metadata = {
|
|
2772
|
+
version: AUTH_STATE_SCHEMA_VERSION,
|
|
2773
|
+
kind: "web",
|
|
2774
|
+
target: input3.paths.targetName,
|
|
2775
|
+
name: input3.paths.stateName,
|
|
2776
|
+
capturedAt
|
|
2777
|
+
};
|
|
2778
|
+
const { writeAuthStateFiles: writeAuthStateFiles2 } = await import("@vostride/agent-qa-core");
|
|
2779
|
+
await writeAuthStateFiles2(input3.paths, { payload, metadata });
|
|
2780
|
+
return {
|
|
2781
|
+
version: metadata.version,
|
|
2782
|
+
kind: metadata.kind,
|
|
2783
|
+
targetName: metadata.target,
|
|
2784
|
+
stateName: metadata.name,
|
|
2785
|
+
capturedAt: metadata.capturedAt,
|
|
2786
|
+
storageStatePath: input3.paths.payloadPath
|
|
2787
|
+
};
|
|
2788
|
+
}
|
|
2695
2789
|
async function executeSuites(suiteFiles, opts, config, globalOpts, configContent, effectiveLogLevel, variableContext, hooksContext) {
|
|
2696
2790
|
const {
|
|
2697
2791
|
parseSuiteFile: parseSuiteFile2,
|
|
@@ -2792,6 +2886,13 @@ async function executeSuites(suiteFiles, opts, config, globalOpts, configContent
|
|
|
2792
2886
|
suiteFileContent = await readFile5(suiteFile, "utf-8");
|
|
2793
2887
|
} catch {
|
|
2794
2888
|
}
|
|
2889
|
+
const mergedUse = mergeUseBlocks(
|
|
2890
|
+
config.use,
|
|
2891
|
+
suite.use,
|
|
2892
|
+
void 0,
|
|
2893
|
+
{}
|
|
2894
|
+
);
|
|
2895
|
+
const suiteAuthStateUse = getSelectedAuthStateUse(mergedUse);
|
|
2795
2896
|
const testEntries = [];
|
|
2796
2897
|
const suiteMembers = [];
|
|
2797
2898
|
for (const { test: relativePath, id: suiteEntryId } of suite.tests) {
|
|
@@ -2911,6 +3012,11 @@ async function executeSuites(suiteFiles, opts, config, globalOpts, configContent
|
|
|
2911
3012
|
process.exit(2);
|
|
2912
3013
|
}
|
|
2913
3014
|
const testDef = parseResult.tests[0];
|
|
3015
|
+
const childAuthStateUse = getSelectedAuthStateUse(testDef.use);
|
|
3016
|
+
if (suiteAuthStateUse && childAuthStateUse && childAuthStateUse.name !== suiteAuthStateUse.name) {
|
|
3017
|
+
console.error(pc7.red(`Suite auth state "${suiteAuthStateUse.name}" conflicts with child test auth state "${childAuthStateUse.name}". Use one primary auth state per suite.`));
|
|
3018
|
+
process.exit(2);
|
|
3019
|
+
}
|
|
2914
3020
|
if (testDef["test-id"] && testDef["test-id"] !== suiteEntryId) {
|
|
2915
3021
|
const mismatchMsg = `ID mismatch in suite "${suite.name}": test file ${relativePath} has test-id "${testDef["test-id"]}" but suite entry specifies id "${suiteEntryId}"`;
|
|
2916
3022
|
console.error(pc7.red(mismatchMsg));
|
|
@@ -2962,12 +3068,18 @@ async function executeSuites(suiteFiles, opts, config, globalOpts, configContent
|
|
|
2962
3068
|
}
|
|
2963
3069
|
const suiteResolvedTarget = suite.target ? resolveTarget(config, suite.target) : resolveTarget(config, testEntries[0][0].target);
|
|
2964
3070
|
const suitePlatform = suiteResolvedTarget.platform;
|
|
2965
|
-
const
|
|
2966
|
-
config
|
|
2967
|
-
|
|
2968
|
-
|
|
2969
|
-
|
|
2970
|
-
);
|
|
3071
|
+
const suiteAuthState = await resolveSelectedAuthStateForRun({
|
|
3072
|
+
config,
|
|
3073
|
+
configDir,
|
|
3074
|
+
resolvedTarget: suiteResolvedTarget,
|
|
3075
|
+
authState: suiteAuthStateUse
|
|
3076
|
+
});
|
|
3077
|
+
const suiteAuthStateCapturePaths = resolveAuthStateCapturePathsForRun({
|
|
3078
|
+
config,
|
|
3079
|
+
configDir,
|
|
3080
|
+
resolvedTarget: suiteResolvedTarget,
|
|
3081
|
+
authState: suiteAuthStateUse
|
|
3082
|
+
});
|
|
2971
3083
|
const suiteEffectiveCacheEnabled = opts.cache !== false && mergedUse.cache !== false;
|
|
2972
3084
|
let suiteDeviceName = opts.device ?? mergedUse.device;
|
|
2973
3085
|
if ((suitePlatform === "android" || suitePlatform === "ios") && !suiteDeviceName) {
|
|
@@ -3051,6 +3163,7 @@ async function executeSuites(suiteFiles, opts, config, globalOpts, configContent
|
|
|
3051
3163
|
const suiteBrowser = mergedUse.browser ?? config.use?.browser;
|
|
3052
3164
|
const suiteLogCapture = mergedUse.logCapture ?? config.use?.logCapture;
|
|
3053
3165
|
const platformConfig = buildPlatformConfig(suitePlatform, resolvedDevice, testTimeouts, suiteBrowser, suiteLogCapture, farmSession, mobileResolved);
|
|
3166
|
+
platformConfig.authState = suiteAuthState;
|
|
3054
3167
|
platformConfig.verbose = effectiveLogLevel === "debug";
|
|
3055
3168
|
if (opts.headless !== void 0 && platformConfig.browser) {
|
|
3056
3169
|
platformConfig.browser.headless = opts.headless;
|
|
@@ -3241,6 +3354,10 @@ async function executeSuites(suiteFiles, opts, config, globalOpts, configContent
|
|
|
3241
3354
|
memoryRoot: suiteMemoryRoot,
|
|
3242
3355
|
circuitBreaker: suiteCircuitBreaker,
|
|
3243
3356
|
product: resolvedTarget?.product,
|
|
3357
|
+
authStateCapture: suiteAuthStateCapturePaths ? {
|
|
3358
|
+
capture: () => captureAuthStateForRun({ adapter, paths: suiteAuthStateCapturePaths }),
|
|
3359
|
+
failureSummary: formatAuthStateCaptureFailure(suiteAuthStateCapturePaths)
|
|
3360
|
+
} : void 0,
|
|
3244
3361
|
createAdapter: () => createPlatformAdapter(suitePlatform),
|
|
3245
3362
|
resolveUrl: (targetName) => {
|
|
3246
3363
|
try {
|
|
@@ -3570,7 +3687,7 @@ Discovered ${files.length} test file(s):
|
|
|
3570
3687
|
dashboardDb.insertRunArtifact({
|
|
3571
3688
|
runId: artifactRunId,
|
|
3572
3689
|
kind: "test",
|
|
3573
|
-
payload: {
|
|
3690
|
+
payload: redactAuthStateValue({
|
|
3574
3691
|
config: {
|
|
3575
3692
|
rawConfigContent: configContent,
|
|
3576
3693
|
parsedConfig: config,
|
|
@@ -3601,7 +3718,7 @@ Discovered ${files.length} test file(s):
|
|
|
3601
3718
|
message: error.message,
|
|
3602
3719
|
details: error
|
|
3603
3720
|
}))
|
|
3604
|
-
}
|
|
3721
|
+
})
|
|
3605
3722
|
});
|
|
3606
3723
|
dashboardDb.finalizeRunArtifact(artifactRunId);
|
|
3607
3724
|
dashboardDb.close();
|
|
@@ -3773,6 +3890,20 @@ Config for: ${test.name}`));
|
|
|
3773
3890
|
}
|
|
3774
3891
|
const testPlatform = testResolvedTarget.platform;
|
|
3775
3892
|
analyticsPlatform = testPlatform;
|
|
3893
|
+
const testAuthStateUse = getSelectedAuthStateUse(mergedUse);
|
|
3894
|
+
const testAuthState = await resolveSelectedAuthStateForRun({
|
|
3895
|
+
config,
|
|
3896
|
+
configDir,
|
|
3897
|
+
resolvedTarget: testResolvedTarget,
|
|
3898
|
+
authState: testAuthStateUse
|
|
3899
|
+
});
|
|
3900
|
+
const testAuthStateCapturePaths = resolveAuthStateCapturePathsForRun({
|
|
3901
|
+
config,
|
|
3902
|
+
configDir,
|
|
3903
|
+
resolvedTarget: testResolvedTarget,
|
|
3904
|
+
authState: testAuthStateUse
|
|
3905
|
+
});
|
|
3906
|
+
let activeTestAuthState = testAuthState;
|
|
3776
3907
|
const testMergedUse = testMergedConfig.use ?? config.use ?? {};
|
|
3777
3908
|
const testDeviceName = opts.device ?? testMergedUse.device;
|
|
3778
3909
|
const testTimeoutsForSetup = {
|
|
@@ -3852,6 +3983,7 @@ Config for: ${test.name}`));
|
|
|
3852
3983
|
adapter = await createPlatformAdapter(testPlatform);
|
|
3853
3984
|
const mergedBrowser = mergedUse.browser;
|
|
3854
3985
|
const platformConfig = buildPlatformConfig(testPlatform, testResolvedDevice, testTimeoutsForSetup, mergedBrowser ?? config.use?.browser, testMergedConfig.use?.logCapture ?? config.use?.logCapture, testFarmSession, testMobileResolved);
|
|
3986
|
+
platformConfig.authState = activeTestAuthState;
|
|
3855
3987
|
platformConfig.verbose = effectiveLogLevel === "debug";
|
|
3856
3988
|
if (opts.headless !== void 0) {
|
|
3857
3989
|
if (platformConfig.browser) {
|
|
@@ -3970,10 +4102,12 @@ Config for: ${test.name}`));
|
|
|
3970
4102
|
runId,
|
|
3971
4103
|
artifact: artifactContext.artifact
|
|
3972
4104
|
});
|
|
4105
|
+
const getAuthAwareSandboxOptions = () => sandboxOptions && activeTestAuthState ? { ...sandboxOptions, authState: activeTestAuthState } : sandboxOptions;
|
|
4106
|
+
const authAwareSandboxOptions = getAuthAwareSandboxOptions();
|
|
3973
4107
|
let perTestSetupVars = {};
|
|
3974
4108
|
let testStartTime = Date.now();
|
|
3975
4109
|
let setupHookFailed = false;
|
|
3976
|
-
if (resolvedHooks &&
|
|
4110
|
+
if (resolvedHooks && authAwareSandboxOptions && testDef.setup?.length) {
|
|
3977
4111
|
const { runHooks } = await import("@vostride/agent-qa-core");
|
|
3978
4112
|
const hookDefs = [];
|
|
3979
4113
|
let hookSetupError;
|
|
@@ -3993,7 +4127,7 @@ Config for: ${test.name}`));
|
|
|
3993
4127
|
for (const hookDef of hookDefs) {
|
|
3994
4128
|
const hookExecId = randomUUID();
|
|
3995
4129
|
await multiReporter.onHookStart({ hookId: hookDef.id, hookName: hookDef.name, phase: "setup", hookExecutionId: hookExecId, runId });
|
|
3996
|
-
const hookResult = await runHooks([hookDef], { ...
|
|
4130
|
+
const hookResult = await runHooks([hookDef], { ...authAwareSandboxOptions, envVars: { ...authAwareSandboxOptions.envVars, ...allVars, ...perTestSetupVars } });
|
|
3997
4131
|
const hr = hookResult.results.get(hookDef.name);
|
|
3998
4132
|
await multiReporter.onHookEnd({
|
|
3999
4133
|
hookId: hookDef.id,
|
|
@@ -4080,7 +4214,7 @@ Config for: ${test.name}`));
|
|
|
4080
4214
|
cliVars,
|
|
4081
4215
|
hookSetupVars: perTestSetupVars,
|
|
4082
4216
|
inlineHookDefs: resolvedHooks,
|
|
4083
|
-
inlineHookSandboxOptions:
|
|
4217
|
+
inlineHookSandboxOptions: authAwareSandboxOptions,
|
|
4084
4218
|
logCapture: testLogCapture,
|
|
4085
4219
|
accessibility: config.services?.accessibility,
|
|
4086
4220
|
accessibilityCheck: runWebAccessibilityCheck,
|
|
@@ -4091,11 +4225,28 @@ Config for: ${test.name}`));
|
|
|
4091
4225
|
memoryInitParams,
|
|
4092
4226
|
circuitBreaker,
|
|
4093
4227
|
runId,
|
|
4094
|
-
skipReporterOnTestStart: true
|
|
4228
|
+
skipReporterOnTestStart: true,
|
|
4229
|
+
skipReporterOnTestEnd: testAuthStateUse?.capture === true
|
|
4095
4230
|
}, filePath);
|
|
4096
4231
|
if (!result.runId) result.runId = runId;
|
|
4097
4232
|
completedResult = result;
|
|
4098
|
-
if (
|
|
4233
|
+
if (testAuthStateCapturePaths && result.status === "passed") {
|
|
4234
|
+
try {
|
|
4235
|
+
activeTestAuthState = await captureAuthStateForRun({
|
|
4236
|
+
adapter,
|
|
4237
|
+
paths: testAuthStateCapturePaths
|
|
4238
|
+
});
|
|
4239
|
+
} catch {
|
|
4240
|
+
result.status = "failed";
|
|
4241
|
+
result.failureSummary = formatAuthStateCaptureFailure(testAuthStateCapturePaths);
|
|
4242
|
+
result.metadata = {
|
|
4243
|
+
...result.metadata,
|
|
4244
|
+
phase: "auth-state-capture"
|
|
4245
|
+
};
|
|
4246
|
+
}
|
|
4247
|
+
}
|
|
4248
|
+
const teardownSandboxOptions = getAuthAwareSandboxOptions();
|
|
4249
|
+
if (resolvedHooks && teardownSandboxOptions && testDef.teardown?.length) {
|
|
4099
4250
|
const { runHooks } = await import("@vostride/agent-qa-core");
|
|
4100
4251
|
for (const hookId of testDef.teardown) {
|
|
4101
4252
|
const hook = resolvedHooks.get(hookId);
|
|
@@ -4105,7 +4256,7 @@ Config for: ${test.name}`));
|
|
|
4105
4256
|
await multiReporter.onHookStart({ hookId: hook.id, hookName: hook.name, phase: "teardown", hookExecutionId: hookExecId, runId });
|
|
4106
4257
|
const allVars = { ...envFileVars, ...perTestSetupVars };
|
|
4107
4258
|
if (cliVars) Object.assign(allVars, cliVars);
|
|
4108
|
-
const hookResult = await runHooks([hook], { ...
|
|
4259
|
+
const hookResult = await runHooks([hook], { ...teardownSandboxOptions, envVars: { ...teardownSandboxOptions.envVars, ...allVars } });
|
|
4109
4260
|
const hr = hookResult.results.get(hook.name);
|
|
4110
4261
|
await multiReporter.onHookEnd({
|
|
4111
4262
|
hookId: hook.id,
|
|
@@ -4144,7 +4295,8 @@ Config for: ${test.name}`));
|
|
|
4144
4295
|
};
|
|
4145
4296
|
}
|
|
4146
4297
|
let ablationHandledDeprecation = false;
|
|
4147
|
-
|
|
4298
|
+
const authStateCaptureFailed = result.metadata?.phase === "auth-state-capture";
|
|
4299
|
+
if (memoryProvider && memoryConfig?.ablationEnabled !== false && result.status === "failed" && !authStateCaptureFailed) {
|
|
4148
4300
|
try {
|
|
4149
4301
|
const { shouldAblate, collectAllInjectedIds, deprecateOnFailure } = await import("@vostride/agent-qa-core");
|
|
4150
4302
|
if (shouldAblate(result, memoryProvider)) {
|
|
@@ -4152,6 +4304,7 @@ Config for: ${test.name}`));
|
|
|
4152
4304
|
const ablationAdapter = await createPlatformAdapter(testPlatform);
|
|
4153
4305
|
const mergedBrowser = mergedUse.browser;
|
|
4154
4306
|
const ablationPlatformConfig = buildPlatformConfig(testPlatform, testResolvedDevice, testTimeoutsForSetup, mergedBrowser ?? config.use?.browser, testMergedConfig.use?.logCapture ?? config.use?.logCapture, testFarmSession, testMobileResolved);
|
|
4307
|
+
ablationPlatformConfig.authState = activeTestAuthState;
|
|
4155
4308
|
ablationPlatformConfig.verbose = effectiveLogLevel === "debug";
|
|
4156
4309
|
if (opts.headless !== void 0 && ablationPlatformConfig.browser) {
|
|
4157
4310
|
ablationPlatformConfig.browser.headless = opts.headless;
|
|
@@ -4183,7 +4336,7 @@ Config for: ${test.name}`));
|
|
|
4183
4336
|
cliVars,
|
|
4184
4337
|
hookSetupVars: perTestSetupVars,
|
|
4185
4338
|
inlineHookDefs: resolvedHooks,
|
|
4186
|
-
inlineHookSandboxOptions:
|
|
4339
|
+
inlineHookSandboxOptions: authAwareSandboxOptions,
|
|
4187
4340
|
logCapture: testLogCapture,
|
|
4188
4341
|
accessibility: config.services?.accessibility,
|
|
4189
4342
|
accessibilityCheck: runWebAccessibilityCheck,
|
|
@@ -4250,6 +4403,9 @@ Config for: ${test.name}`));
|
|
|
4250
4403
|
console.warn(` ${pc7.dim("Memory:")} curator error -- ${err.message}`);
|
|
4251
4404
|
}
|
|
4252
4405
|
}
|
|
4406
|
+
if (testAuthStateUse?.capture === true) {
|
|
4407
|
+
await multiReporter.onTestEnd(result);
|
|
4408
|
+
}
|
|
4253
4409
|
results.push(result);
|
|
4254
4410
|
printRunAttributes(testAttributes);
|
|
4255
4411
|
if (circuitBreaker && memoryProvider) {
|
|
@@ -4260,7 +4416,7 @@ Config for: ${test.name}`));
|
|
|
4260
4416
|
circuitBreakerTripped = true;
|
|
4261
4417
|
console.log(` ${pc7.yellow("Memory: circuit breaker tripped -- disabling memory injection for remaining tests")}`);
|
|
4262
4418
|
}
|
|
4263
|
-
if (result.status === "failed" && actionCache && effectiveCacheEnabled && result.steps.length > 0) {
|
|
4419
|
+
if (result.status === "failed" && !authStateCaptureFailed && actionCache && effectiveCacheEnabled && result.steps.length > 0) {
|
|
4264
4420
|
const { hashStepInstruction: hashStepInstruction2 } = await import("@vostride/agent-qa-core");
|
|
4265
4421
|
const { rm: rm3 } = await import("fs/promises");
|
|
4266
4422
|
const { resolve: resolve9, join: join8 } = await import("path");
|
|
@@ -4391,7 +4547,7 @@ ${errStack}`;
|
|
|
4391
4547
|
status: finalStatus,
|
|
4392
4548
|
duration: run.startedAt ? Date.now() - new Date(run.startedAt).getTime() : 0,
|
|
4393
4549
|
endedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4394
|
-
failureSummary: isBrowserClose ? "Browser closed by user" : formattedError.failureSummary
|
|
4550
|
+
failureSummary: isBrowserClose ? "Browser closed by user" : redactAuthStateValue(formattedError.failureSummary)
|
|
4395
4551
|
});
|
|
4396
4552
|
try {
|
|
4397
4553
|
const artifact = _crashDb.getRunArtifact(run.id);
|
|
@@ -4404,7 +4560,7 @@ ${errStack}`;
|
|
|
4404
4560
|
errors: [{
|
|
4405
4561
|
code: isBrowserClose ? "browser-disconnect" : "process-close",
|
|
4406
4562
|
phase: "process-close",
|
|
4407
|
-
message: isBrowserClose ? "Browser closed by user" : formattedError.failureSummary
|
|
4563
|
+
message: isBrowserClose ? "Browser closed by user" : redactAuthStateValue(formattedError.failureSummary)
|
|
4408
4564
|
}]
|
|
4409
4565
|
});
|
|
4410
4566
|
}
|
|
@@ -5199,37 +5355,267 @@ function createAuthCommand() {
|
|
|
5199
5355
|
return cmd;
|
|
5200
5356
|
}
|
|
5201
5357
|
|
|
5202
|
-
// src/commands/
|
|
5358
|
+
// src/commands/auth-state.ts
|
|
5203
5359
|
init_config();
|
|
5360
|
+
import { createInterface as createInterface3 } from "readline/promises";
|
|
5361
|
+
import { dirname as dirname7, resolve as resolvePath4 } from "path";
|
|
5204
5362
|
import { Command as Command13 } from "commander";
|
|
5363
|
+
import pc13 from "picocolors";
|
|
5364
|
+
import {
|
|
5365
|
+
AUTH_STATE_SCHEMA_VERSION as AUTH_STATE_SCHEMA_VERSION2,
|
|
5366
|
+
listAuthStateMetadata,
|
|
5367
|
+
readAuthStateMetadata,
|
|
5368
|
+
removeAuthStateFiles,
|
|
5369
|
+
removeAuthStateTarget,
|
|
5370
|
+
resolveAuthStatePaths as resolveAuthStatePaths2,
|
|
5371
|
+
writeAuthStateFiles
|
|
5372
|
+
} from "@vostride/agent-qa-core";
|
|
5373
|
+
function getGlobalConfigPath3(command) {
|
|
5374
|
+
return command.parent?.parent?.opts().config ?? command.parent?.opts().config;
|
|
5375
|
+
}
|
|
5376
|
+
function isBrowserName(value) {
|
|
5377
|
+
return value === "chromium" || value === "firefox" || value === "webkit";
|
|
5378
|
+
}
|
|
5379
|
+
function resolveBrowserName(config) {
|
|
5380
|
+
const name = config.use?.browser?.name;
|
|
5381
|
+
return isBrowserName(name) ? name : "chromium";
|
|
5382
|
+
}
|
|
5383
|
+
async function loadPlaywrightLaunchers() {
|
|
5384
|
+
const { chromium, firefox, webkit } = await import("playwright-core");
|
|
5385
|
+
return { chromium, firefox, webkit };
|
|
5386
|
+
}
|
|
5387
|
+
async function waitForCaptureConfirmation(input3) {
|
|
5388
|
+
console.log(input3.message);
|
|
5389
|
+
const rl = createInterface3({ input: process.stdin, output: process.stdout });
|
|
5390
|
+
return await new Promise((resolve9) => {
|
|
5391
|
+
let settled = false;
|
|
5392
|
+
const finish = (result) => {
|
|
5393
|
+
if (settled) return;
|
|
5394
|
+
settled = true;
|
|
5395
|
+
process.off("SIGINT", onSigint);
|
|
5396
|
+
input3.browser.off?.("disconnected", onDisconnected);
|
|
5397
|
+
rl.close();
|
|
5398
|
+
resolve9(result);
|
|
5399
|
+
};
|
|
5400
|
+
const onSigint = () => finish("cancelled");
|
|
5401
|
+
const onDisconnected = () => finish("browser-closed");
|
|
5402
|
+
process.once("SIGINT", onSigint);
|
|
5403
|
+
input3.browser.on("disconnected", onDisconnected);
|
|
5404
|
+
rl.question("").then(() => finish("confirmed")).catch(() => finish("cancelled"));
|
|
5405
|
+
});
|
|
5406
|
+
}
|
|
5407
|
+
async function authStateExists(paths) {
|
|
5408
|
+
try {
|
|
5409
|
+
await readAuthStateMetadata(paths);
|
|
5410
|
+
return true;
|
|
5411
|
+
} catch {
|
|
5412
|
+
return false;
|
|
5413
|
+
}
|
|
5414
|
+
}
|
|
5415
|
+
function buildConfirmationMessage(targetName, stateName, exists) {
|
|
5416
|
+
const replace = exists ? " Existing auth state will be replaced." : "";
|
|
5417
|
+
return `Log in in the opened browser, then press Enter to save auth state "${stateName}" for target "${targetName}".${replace} Press Ctrl+C to cancel.`;
|
|
5418
|
+
}
|
|
5419
|
+
async function loadCommandContext(command) {
|
|
5420
|
+
const configPath = getGlobalConfigPath3(command) ?? "agent-qa.config.yaml";
|
|
5421
|
+
const configDir = dirname7(resolvePath4(configPath));
|
|
5422
|
+
const config = await resolveConfig({ configPath, loadAuthPlugins: false });
|
|
5423
|
+
return { configPath, configDir, config };
|
|
5424
|
+
}
|
|
5425
|
+
function isSafeAuthStateError(message) {
|
|
5426
|
+
return message.includes("auth state is only supported for web targets") || message.includes("use.mobile.appState: preserve") || message.includes("Auth state name must match") || message.includes("Target name must match");
|
|
5427
|
+
}
|
|
5428
|
+
function printSafeAuthStateError(error, fallback) {
|
|
5429
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
5430
|
+
console.error(pc13.red(isSafeAuthStateError(message) ? message : fallback));
|
|
5431
|
+
}
|
|
5432
|
+
function assertWebAuthStateTarget(input3) {
|
|
5433
|
+
resolveAuthStatePaths2({
|
|
5434
|
+
configDir: input3.configDir,
|
|
5435
|
+
authStateDir: input3.config.services?.authState?.dir,
|
|
5436
|
+
targetName: input3.target.name,
|
|
5437
|
+
stateName: "placeholder",
|
|
5438
|
+
target: { platform: input3.target.platform }
|
|
5439
|
+
});
|
|
5440
|
+
}
|
|
5441
|
+
function printAuthStateList(metadata, targetName) {
|
|
5442
|
+
if (metadata.length === 0) {
|
|
5443
|
+
console.log(targetName ? `No auth states saved for target "${targetName}".` : "No auth states saved.");
|
|
5444
|
+
return;
|
|
5445
|
+
}
|
|
5446
|
+
console.log("Target Name Captured Kind");
|
|
5447
|
+
for (const entry of metadata) {
|
|
5448
|
+
console.log(`${entry.target} ${entry.name} ${entry.capturedAt} ${entry.kind}`);
|
|
5449
|
+
}
|
|
5450
|
+
}
|
|
5451
|
+
async function closeBrowser(browser) {
|
|
5452
|
+
if (!browser) return;
|
|
5453
|
+
try {
|
|
5454
|
+
await browser.close();
|
|
5455
|
+
} catch {
|
|
5456
|
+
}
|
|
5457
|
+
}
|
|
5458
|
+
function createAuthStateCommand(deps = {}) {
|
|
5459
|
+
const cmd = new Command13("auth-state").description("Manage product auth state");
|
|
5460
|
+
cmd.addCommand(
|
|
5461
|
+
new Command13("list").description("List saved auth states").option("--target <target>", "target name from agent-qa config").action(async (opts, command) => {
|
|
5462
|
+
try {
|
|
5463
|
+
const { configDir, config } = await loadCommandContext(command);
|
|
5464
|
+
let targetName;
|
|
5465
|
+
if (opts.target) {
|
|
5466
|
+
const target = resolveTarget(config, opts.target);
|
|
5467
|
+
assertWebAuthStateTarget({ configDir, config, target });
|
|
5468
|
+
targetName = target.name;
|
|
5469
|
+
}
|
|
5470
|
+
const metadata = await listAuthStateMetadata({
|
|
5471
|
+
configDir,
|
|
5472
|
+
authStateDir: config.services?.authState?.dir,
|
|
5473
|
+
targetName
|
|
5474
|
+
});
|
|
5475
|
+
printAuthStateList(metadata, targetName);
|
|
5476
|
+
} catch (error) {
|
|
5477
|
+
printSafeAuthStateError(error, "Could not list auth states.");
|
|
5478
|
+
process.exitCode = 1;
|
|
5479
|
+
}
|
|
5480
|
+
})
|
|
5481
|
+
);
|
|
5482
|
+
cmd.addCommand(
|
|
5483
|
+
new Command13("remove").description("Remove saved auth state files for a target").requiredOption("--target <target>", "target name from agent-qa config").option("--name <name>", "logical auth-state name").action(async (opts, command) => {
|
|
5484
|
+
let resolvedTargetName = opts.target;
|
|
5485
|
+
try {
|
|
5486
|
+
const { configDir, config } = await loadCommandContext(command);
|
|
5487
|
+
const target = resolveTarget(config, opts.target);
|
|
5488
|
+
resolvedTargetName = target.name;
|
|
5489
|
+
if (opts.name) {
|
|
5490
|
+
await removeAuthStateFiles({
|
|
5491
|
+
configDir,
|
|
5492
|
+
authStateDir: config.services?.authState?.dir,
|
|
5493
|
+
targetName: target.name,
|
|
5494
|
+
stateName: opts.name,
|
|
5495
|
+
target: { platform: target.platform }
|
|
5496
|
+
});
|
|
5497
|
+
console.log(`Removed auth state "${opts.name}" for target "${target.name}".`);
|
|
5498
|
+
return;
|
|
5499
|
+
}
|
|
5500
|
+
await removeAuthStateTarget({
|
|
5501
|
+
configDir,
|
|
5502
|
+
authStateDir: config.services?.authState?.dir,
|
|
5503
|
+
targetName: target.name,
|
|
5504
|
+
target: { platform: target.platform }
|
|
5505
|
+
});
|
|
5506
|
+
console.log(`Removed auth states for target "${target.name}".`);
|
|
5507
|
+
} catch (error) {
|
|
5508
|
+
printSafeAuthStateError(
|
|
5509
|
+
error,
|
|
5510
|
+
opts.name ? `Could not remove auth state "${opts.name}" for target "${resolvedTargetName}".` : `Could not remove auth states for target "${resolvedTargetName}".`
|
|
5511
|
+
);
|
|
5512
|
+
process.exitCode = 1;
|
|
5513
|
+
}
|
|
5514
|
+
})
|
|
5515
|
+
);
|
|
5516
|
+
cmd.addCommand(
|
|
5517
|
+
new Command13("capture").description("Capture a named web auth state for a target").requiredOption("--target <target>", "target name from agent-qa config").requiredOption("--name <name>", "logical auth-state name").action(async (opts, command) => {
|
|
5518
|
+
let browser;
|
|
5519
|
+
try {
|
|
5520
|
+
const { configDir, config } = await loadCommandContext(command);
|
|
5521
|
+
const target = resolveTarget(config, opts.target);
|
|
5522
|
+
const paths = resolveAuthStatePaths2({
|
|
5523
|
+
configDir,
|
|
5524
|
+
authStateDir: config.services?.authState?.dir,
|
|
5525
|
+
targetName: target.name,
|
|
5526
|
+
stateName: opts.name,
|
|
5527
|
+
target: { platform: target.platform }
|
|
5528
|
+
});
|
|
5529
|
+
if (!target.url) {
|
|
5530
|
+
console.error(pc13.red(`Target "${target.name}" must define a web URL for auth-state capture.`));
|
|
5531
|
+
process.exitCode = 1;
|
|
5532
|
+
return;
|
|
5533
|
+
}
|
|
5534
|
+
const launchers = deps.launchers ?? await loadPlaywrightLaunchers();
|
|
5535
|
+
const browserName = resolveBrowserName(config);
|
|
5536
|
+
browser = await launchers[browserName].launch({ headless: false });
|
|
5537
|
+
const context = await browser.newContext();
|
|
5538
|
+
const page = await context.newPage();
|
|
5539
|
+
await page.goto(target.url, { waitUntil: "domcontentloaded" });
|
|
5540
|
+
const exists = await authStateExists(paths);
|
|
5541
|
+
const waitForConfirmation = deps.waitForConfirmation ?? waitForCaptureConfirmation;
|
|
5542
|
+
const confirmation = await waitForConfirmation({
|
|
5543
|
+
browser,
|
|
5544
|
+
message: buildConfirmationMessage(target.name, opts.name, exists)
|
|
5545
|
+
});
|
|
5546
|
+
if (confirmation === "cancelled") {
|
|
5547
|
+
await closeBrowser(browser);
|
|
5548
|
+
console.log("Auth-state capture cancelled.");
|
|
5549
|
+
process.exitCode = 1;
|
|
5550
|
+
return;
|
|
5551
|
+
}
|
|
5552
|
+
if (confirmation === "browser-closed") {
|
|
5553
|
+
await closeBrowser(browser);
|
|
5554
|
+
console.error(pc13.red("Browser was closed before auth state was saved."));
|
|
5555
|
+
process.exitCode = 1;
|
|
5556
|
+
return;
|
|
5557
|
+
}
|
|
5558
|
+
const payload = await context.storageState({ indexedDB: true });
|
|
5559
|
+
try {
|
|
5560
|
+
await writeAuthStateFiles(paths, {
|
|
5561
|
+
payload,
|
|
5562
|
+
metadata: {
|
|
5563
|
+
version: AUTH_STATE_SCHEMA_VERSION2,
|
|
5564
|
+
kind: "web",
|
|
5565
|
+
target: target.name,
|
|
5566
|
+
name: opts.name,
|
|
5567
|
+
capturedAt: (deps.now ?? (() => /* @__PURE__ */ new Date()))().toISOString()
|
|
5568
|
+
}
|
|
5569
|
+
});
|
|
5570
|
+
} catch {
|
|
5571
|
+
await closeBrowser(browser);
|
|
5572
|
+
console.error(pc13.red(`Could not save auth state "${opts.name}" for target "${target.name}".`));
|
|
5573
|
+
process.exitCode = 1;
|
|
5574
|
+
return;
|
|
5575
|
+
}
|
|
5576
|
+
await closeBrowser(browser);
|
|
5577
|
+
console.log(`Saved auth state "${opts.name}" for target "${target.name}".`);
|
|
5578
|
+
} catch (error) {
|
|
5579
|
+
await closeBrowser(browser);
|
|
5580
|
+
console.error(pc13.red(error instanceof Error ? error.message : String(error)));
|
|
5581
|
+
process.exitCode = 1;
|
|
5582
|
+
}
|
|
5583
|
+
})
|
|
5584
|
+
);
|
|
5585
|
+
return cmd;
|
|
5586
|
+
}
|
|
5587
|
+
|
|
5588
|
+
// src/commands/devices.ts
|
|
5589
|
+
init_config();
|
|
5590
|
+
import { Command as Command14 } from "commander";
|
|
5205
5591
|
import { execSync as execSync2 } from "child_process";
|
|
5206
5592
|
import { existsSync as existsSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
|
|
5207
5593
|
import { join as join5 } from "path";
|
|
5208
5594
|
import { stringify as stringify2 } from "yaml";
|
|
5209
|
-
import
|
|
5595
|
+
import pc14 from "picocolors";
|
|
5210
5596
|
function formatStatus2(transport, localBinding) {
|
|
5211
5597
|
if (transport !== "local") {
|
|
5212
|
-
return
|
|
5598
|
+
return pc14.green("Ready") + pc14.dim(" (no local binding required)");
|
|
5213
5599
|
}
|
|
5214
5600
|
if (!localBinding) {
|
|
5215
|
-
return
|
|
5601
|
+
return pc14.red("UNBOUND") + pc14.yellow(" - run `agent-qa devices init`");
|
|
5216
5602
|
}
|
|
5217
5603
|
const keys = Object.entries(localBinding).slice(0, 2);
|
|
5218
5604
|
const details = keys.map(([k, v]) => `${k}: ${v}`).join(", ");
|
|
5219
|
-
return
|
|
5605
|
+
return pc14.green("Bound") + pc14.dim(` (${details})`);
|
|
5220
5606
|
}
|
|
5221
5607
|
async function listDevices() {
|
|
5222
5608
|
let config;
|
|
5223
5609
|
try {
|
|
5224
5610
|
config = await resolveConfig({});
|
|
5225
5611
|
} catch (err) {
|
|
5226
|
-
console.error(
|
|
5612
|
+
console.error(pc14.red(`Config error: ${err instanceof Error ? err.message : String(err)}`));
|
|
5227
5613
|
process.exitCode = 1;
|
|
5228
5614
|
return;
|
|
5229
5615
|
}
|
|
5230
5616
|
const devices = config.registry?.devices;
|
|
5231
5617
|
if (!devices || Object.keys(devices).length === 0) {
|
|
5232
|
-
console.log(
|
|
5618
|
+
console.log(pc14.yellow("No devices configured in registry.devices"));
|
|
5233
5619
|
return;
|
|
5234
5620
|
}
|
|
5235
5621
|
const localBindings = loadLocalBindings();
|
|
@@ -5237,13 +5623,13 @@ async function listDevices() {
|
|
|
5237
5623
|
const platWidth = 10;
|
|
5238
5624
|
const transWidth = 14;
|
|
5239
5625
|
console.log("");
|
|
5240
|
-
console.log(
|
|
5626
|
+
console.log(pc14.bold(" Configured Devices"));
|
|
5241
5627
|
console.log("");
|
|
5242
5628
|
console.log(
|
|
5243
|
-
` ${
|
|
5629
|
+
` ${pc14.dim("Name".padEnd(nameWidth))} ${pc14.dim("Platform".padEnd(platWidth))} ${pc14.dim("Transport".padEnd(transWidth))} ${pc14.dim("Status")}`
|
|
5244
5630
|
);
|
|
5245
5631
|
console.log(
|
|
5246
|
-
` ${
|
|
5632
|
+
` ${pc14.dim("\u2500".repeat(nameWidth))} ${pc14.dim("\u2500".repeat(platWidth))} ${pc14.dim("\u2500".repeat(transWidth))} ${pc14.dim("\u2500".repeat(30))}`
|
|
5247
5633
|
);
|
|
5248
5634
|
for (const [name, profile] of Object.entries(devices)) {
|
|
5249
5635
|
const localBinding = localBindings?.devices?.[name] ?? null;
|
|
@@ -5307,28 +5693,28 @@ async function initDevices() {
|
|
|
5307
5693
|
const dir = process.cwd();
|
|
5308
5694
|
const localFilePath = join5(dir, "agent-qa.local.yaml");
|
|
5309
5695
|
if (existsSync3(localFilePath)) {
|
|
5310
|
-
console.log(
|
|
5696
|
+
console.log(pc14.yellow("agent-qa.local.yaml already exists. Skipping generation."));
|
|
5311
5697
|
return;
|
|
5312
5698
|
}
|
|
5313
|
-
console.log(
|
|
5699
|
+
console.log(pc14.bold("\n Scanning for connected devices...\n"));
|
|
5314
5700
|
const adbDevices = scanAdbDevices();
|
|
5315
5701
|
const simctlDevices = scanSimctlDevices();
|
|
5316
5702
|
if (adbDevices.length > 0) {
|
|
5317
|
-
console.log(` ${
|
|
5703
|
+
console.log(` ${pc14.green("Android:")} found ${adbDevices.length} device(s)`);
|
|
5318
5704
|
for (const d of adbDevices) {
|
|
5319
|
-
console.log(` ${
|
|
5705
|
+
console.log(` ${pc14.dim("\u2022")} ${d.model} (${d.serial})`);
|
|
5320
5706
|
}
|
|
5321
5707
|
} else {
|
|
5322
|
-
console.log(` ${
|
|
5708
|
+
console.log(` ${pc14.dim("Android:")} no devices found (adb not available or no devices connected)`);
|
|
5323
5709
|
}
|
|
5324
5710
|
if (simctlDevices.length > 0) {
|
|
5325
5711
|
const booted = simctlDevices.filter((d) => d.state === "Booted");
|
|
5326
|
-
console.log(` ${
|
|
5712
|
+
console.log(` ${pc14.green("iOS:")} found ${simctlDevices.length} simulator(s) (${booted.length} booted)`);
|
|
5327
5713
|
for (const d of booted) {
|
|
5328
|
-
console.log(` ${
|
|
5714
|
+
console.log(` ${pc14.dim("\u2022")} ${d.name} (${d.udid})`);
|
|
5329
5715
|
}
|
|
5330
5716
|
} else {
|
|
5331
|
-
console.log(` ${
|
|
5717
|
+
console.log(` ${pc14.dim("iOS:")} no simulators found (simctl not available or none installed)`);
|
|
5332
5718
|
}
|
|
5333
5719
|
let config;
|
|
5334
5720
|
try {
|
|
@@ -5362,20 +5748,20 @@ async function initDevices() {
|
|
|
5362
5748
|
writeFileSync2(localFilePath, yamlContent);
|
|
5363
5749
|
appendGitignoreEntry(dir, "agent-qa.local.yaml");
|
|
5364
5750
|
console.log("");
|
|
5365
|
-
console.log(
|
|
5366
|
-
console.log(
|
|
5751
|
+
console.log(pc14.green(` Generated agent-qa.local.yaml with ${bindCount} device binding(s)`));
|
|
5752
|
+
console.log(pc14.dim(" Added agent-qa.local.yaml to .gitignore"));
|
|
5367
5753
|
console.log("");
|
|
5368
5754
|
}
|
|
5369
5755
|
function createDevicesCommand() {
|
|
5370
|
-
const cmd = new
|
|
5756
|
+
const cmd = new Command14("devices").description("Manage device profiles and local bindings");
|
|
5371
5757
|
cmd.command("list").description("Show configured devices with merged shared + local view").action(listDevices);
|
|
5372
5758
|
cmd.command("init").description("Scan connected devices and generate agent-qa.local.yaml").action(initDevices);
|
|
5373
5759
|
return cmd;
|
|
5374
5760
|
}
|
|
5375
5761
|
|
|
5376
5762
|
// src/commands/ids.ts
|
|
5377
|
-
import { Command as
|
|
5378
|
-
import
|
|
5763
|
+
import { Command as Command15 } from "commander";
|
|
5764
|
+
import pc15 from "picocolors";
|
|
5379
5765
|
import {
|
|
5380
5766
|
ENTITY_ID_TYPES,
|
|
5381
5767
|
generateCanonicalId,
|
|
@@ -5389,7 +5775,7 @@ function parseEntityType(value) {
|
|
|
5389
5775
|
throw new Error(`Unknown ID type "${value}". Expected one of: ${ENTITY_ID_TYPES.join(", ")}`);
|
|
5390
5776
|
}
|
|
5391
5777
|
function createIdsCommand() {
|
|
5392
|
-
const cmd = new
|
|
5778
|
+
const cmd = new Command15("ids").description("Generate and validate canonical agent-qa entity IDs");
|
|
5393
5779
|
cmd.command("generate").description("Generate a canonical agent-qa ID using id-agent").argument("<type>", `entity type: ${ENTITY_ID_TYPES.join("|")}`).option("--json", "print structured JSON").action((typeValue, opts) => {
|
|
5394
5780
|
try {
|
|
5395
5781
|
const type = parseEntityType(typeValue);
|
|
@@ -5401,7 +5787,7 @@ function createIdsCommand() {
|
|
|
5401
5787
|
console.log(id);
|
|
5402
5788
|
}
|
|
5403
5789
|
} catch (err) {
|
|
5404
|
-
console.error(
|
|
5790
|
+
console.error(pc15.red(err instanceof Error ? err.message : String(err)));
|
|
5405
5791
|
process.exitCode = 1;
|
|
5406
5792
|
}
|
|
5407
5793
|
});
|
|
@@ -5420,15 +5806,15 @@ function createIdsCommand() {
|
|
|
5420
5806
|
if (opts.json) {
|
|
5421
5807
|
console.log(JSON.stringify(result, null, 2));
|
|
5422
5808
|
} else if (valid) {
|
|
5423
|
-
console.log(
|
|
5809
|
+
console.log(pc15.green(result.message));
|
|
5424
5810
|
} else {
|
|
5425
|
-
console.error(
|
|
5811
|
+
console.error(pc15.red(result.message));
|
|
5426
5812
|
}
|
|
5427
5813
|
if (!valid) {
|
|
5428
5814
|
process.exitCode = 1;
|
|
5429
5815
|
}
|
|
5430
5816
|
} catch (err) {
|
|
5431
|
-
console.error(
|
|
5817
|
+
console.error(pc15.red(err instanceof Error ? err.message : String(err)));
|
|
5432
5818
|
process.exitCode = 1;
|
|
5433
5819
|
}
|
|
5434
5820
|
});
|
|
@@ -5436,18 +5822,18 @@ function createIdsCommand() {
|
|
|
5436
5822
|
}
|
|
5437
5823
|
|
|
5438
5824
|
// src/commands/create-test.ts
|
|
5439
|
-
import { Command as
|
|
5825
|
+
import { Command as Command16 } from "commander";
|
|
5440
5826
|
import { generateTestId } from "@vostride/agent-qa-ids";
|
|
5441
5827
|
import { stringify as stringify3 } from "yaml";
|
|
5442
5828
|
import { writeFile as writeFile2, mkdir } from "fs/promises";
|
|
5443
5829
|
import { existsSync as existsSync4 } from "fs";
|
|
5444
|
-
import { resolve as resolve6, dirname as
|
|
5445
|
-
import
|
|
5830
|
+
import { resolve as resolve6, dirname as dirname8 } from "path";
|
|
5831
|
+
import pc16 from "picocolors";
|
|
5446
5832
|
function createCreateTestCommand() {
|
|
5447
|
-
const cmd = new
|
|
5833
|
+
const cmd = new Command16("create-test").description("Scaffold a new test YAML file with auto-generated ID").argument("<path>", "output file path for the test YAML").action(async (path) => {
|
|
5448
5834
|
const resolved = resolve6(path);
|
|
5449
5835
|
if (existsSync4(resolved)) {
|
|
5450
|
-
console.error(
|
|
5836
|
+
console.error(pc16.red("Error: File already exists: " + resolved));
|
|
5451
5837
|
process.exitCode = 1;
|
|
5452
5838
|
return;
|
|
5453
5839
|
}
|
|
@@ -5459,27 +5845,27 @@ function createCreateTestCommand() {
|
|
|
5459
5845
|
steps: ["Describe your first test step here"]
|
|
5460
5846
|
};
|
|
5461
5847
|
const yamlContent = stringify3(template);
|
|
5462
|
-
await mkdir(
|
|
5848
|
+
await mkdir(dirname8(resolved), { recursive: true });
|
|
5463
5849
|
await writeFile2(resolved, yamlContent, "utf-8");
|
|
5464
|
-
console.log(
|
|
5465
|
-
console.log(
|
|
5850
|
+
console.log(pc16.green("Created " + resolved));
|
|
5851
|
+
console.log(pc16.dim(" test-id: " + testId));
|
|
5466
5852
|
});
|
|
5467
5853
|
return cmd;
|
|
5468
5854
|
}
|
|
5469
5855
|
|
|
5470
5856
|
// src/commands/create-suite.ts
|
|
5471
|
-
import { Command as
|
|
5857
|
+
import { Command as Command17 } from "commander";
|
|
5472
5858
|
import { generateSuiteId } from "@vostride/agent-qa-ids";
|
|
5473
5859
|
import { stringify as stringify4 } from "yaml";
|
|
5474
5860
|
import { writeFile as writeFile3, mkdir as mkdir2 } from "fs/promises";
|
|
5475
5861
|
import { existsSync as existsSync5 } from "fs";
|
|
5476
|
-
import { resolve as resolve7, dirname as
|
|
5477
|
-
import
|
|
5862
|
+
import { resolve as resolve7, dirname as dirname9 } from "path";
|
|
5863
|
+
import pc17 from "picocolors";
|
|
5478
5864
|
function createCreateSuiteCommand() {
|
|
5479
|
-
const cmd = new
|
|
5865
|
+
const cmd = new Command17("create-suite").description("Scaffold a new suite YAML file with auto-generated ID").argument("<path>", "output file path for the suite YAML").action(async (path) => {
|
|
5480
5866
|
const resolved = resolve7(path);
|
|
5481
5867
|
if (existsSync5(resolved)) {
|
|
5482
|
-
console.error(
|
|
5868
|
+
console.error(pc17.red("Error: File already exists: " + resolved));
|
|
5483
5869
|
process.exitCode = 1;
|
|
5484
5870
|
return;
|
|
5485
5871
|
}
|
|
@@ -5493,19 +5879,19 @@ function createCreateSuiteCommand() {
|
|
|
5493
5879
|
"tests: []",
|
|
5494
5880
|
"tests: []\n # - test: ./path/to/test.yaml\n # id: my-test-id"
|
|
5495
5881
|
);
|
|
5496
|
-
await mkdir2(
|
|
5882
|
+
await mkdir2(dirname9(resolved), { recursive: true });
|
|
5497
5883
|
await writeFile3(resolved, yamlContent, "utf-8");
|
|
5498
|
-
console.log(
|
|
5499
|
-
console.log(
|
|
5884
|
+
console.log(pc17.green("Created " + resolved));
|
|
5885
|
+
console.log(pc17.dim(" suite-id: " + suiteId));
|
|
5500
5886
|
});
|
|
5501
5887
|
return cmd;
|
|
5502
5888
|
}
|
|
5503
5889
|
|
|
5504
5890
|
// src/commands/clean-memory.ts
|
|
5505
5891
|
init_config();
|
|
5506
|
-
import { Command as
|
|
5507
|
-
import
|
|
5508
|
-
import { createInterface as
|
|
5892
|
+
import { Command as Command18 } from "commander";
|
|
5893
|
+
import pc18 from "picocolors";
|
|
5894
|
+
import { createInterface as createInterface4 } from "readline";
|
|
5509
5895
|
import { readFile as readFile4, readdir as readdir2, rm as rm2, stat as stat2 } from "fs/promises";
|
|
5510
5896
|
import { existsSync as existsSync6 } from "fs";
|
|
5511
5897
|
import { join as join6 } from "path";
|
|
@@ -5561,7 +5947,7 @@ async function getOrphanInfo(memoryRoot, tier, scope) {
|
|
|
5561
5947
|
};
|
|
5562
5948
|
}
|
|
5563
5949
|
function createCleanMemoryCommand() {
|
|
5564
|
-
const cmd = new
|
|
5950
|
+
const cmd = new Command18("clean-memory").description("Remove orphaned memory observation directories").option("-y, --yes", "skip confirmation prompt").action(async (_opts, command) => {
|
|
5565
5951
|
const opts = command.opts();
|
|
5566
5952
|
const configPath = command.parent?.opts()?.config ?? "agent-qa.config.yaml";
|
|
5567
5953
|
const config = await resolveConfig({ configPath });
|
|
@@ -5594,7 +5980,7 @@ function createCleanMemoryCommand() {
|
|
|
5594
5980
|
}
|
|
5595
5981
|
const memoryRoot = resolveMemoryRoot2(config, workspace.configDir);
|
|
5596
5982
|
if (!existsSync6(memoryRoot)) {
|
|
5597
|
-
console.log(
|
|
5983
|
+
console.log(pc18.dim("No memory directory found."));
|
|
5598
5984
|
return;
|
|
5599
5985
|
}
|
|
5600
5986
|
const TIERS = [
|
|
@@ -5624,22 +6010,22 @@ function createCleanMemoryCommand() {
|
|
|
5624
6010
|
}
|
|
5625
6011
|
}
|
|
5626
6012
|
if (orphans.length === 0) {
|
|
5627
|
-
console.log(
|
|
6013
|
+
console.log(pc18.dim("No orphaned memory directories found."));
|
|
5628
6014
|
return;
|
|
5629
6015
|
}
|
|
5630
6016
|
const maxPathLen = Math.max("Path".length, ...orphans.map((o) => o.path.length));
|
|
5631
6017
|
console.log("");
|
|
5632
|
-
console.log(
|
|
6018
|
+
console.log(pc18.bold("Orphaned memory directories:"));
|
|
5633
6019
|
console.log("");
|
|
5634
|
-
console.log(" " +
|
|
5635
|
-
console.log(" " +
|
|
6020
|
+
console.log(" " + pc18.dim("Path".padEnd(maxPathLen + 2) + "Files".padStart(7) + " " + "Last Confirmed".padEnd(20) + "Size".padStart(10)));
|
|
6021
|
+
console.log(" " + pc18.dim("-".repeat(maxPathLen + 2 + 7 + 2 + 20 + 10)));
|
|
5636
6022
|
for (const o of orphans) {
|
|
5637
|
-
const confirmed = o.lastConfirmed ??
|
|
6023
|
+
const confirmed = o.lastConfirmed ?? pc18.dim("never");
|
|
5638
6024
|
console.log(" " + o.path.padEnd(maxPathLen + 2) + String(o.fileCount).padStart(7) + " " + String(confirmed).padEnd(20) + formatSize(o.size).padStart(10));
|
|
5639
6025
|
}
|
|
5640
6026
|
console.log("");
|
|
5641
6027
|
if (!opts.yes) {
|
|
5642
|
-
const rl =
|
|
6028
|
+
const rl = createInterface4({ input: process.stdin, output: process.stdout });
|
|
5643
6029
|
const answer = await new Promise((res) => {
|
|
5644
6030
|
rl.question(`Delete ${orphans.length} orphaned director${orphans.length === 1 ? "y" : "ies"}? (y/N) `, res);
|
|
5645
6031
|
});
|
|
@@ -5652,15 +6038,15 @@ function createCleanMemoryCommand() {
|
|
|
5652
6038
|
for (const orphan of orphans) {
|
|
5653
6039
|
await rm2(join6(memoryRoot, orphan.tier, orphan.scope), { recursive: true, force: true });
|
|
5654
6040
|
}
|
|
5655
|
-
console.log(
|
|
6041
|
+
console.log(pc18.green(`Deleted ${orphans.length} orphaned director${orphans.length === 1 ? "y" : "ies"}.`));
|
|
5656
6042
|
});
|
|
5657
6043
|
return cmd;
|
|
5658
6044
|
}
|
|
5659
6045
|
|
|
5660
6046
|
// src/commands/skills.ts
|
|
5661
|
-
import { Command as
|
|
6047
|
+
import { Command as Command19 } from "commander";
|
|
5662
6048
|
import { existsSync as existsSync7, readdirSync, readFileSync as readFileSync4, statSync } from "fs";
|
|
5663
|
-
import { dirname as
|
|
6049
|
+
import { dirname as dirname10, join as join7, resolve as resolve8 } from "path";
|
|
5664
6050
|
import { fileURLToPath } from "url";
|
|
5665
6051
|
function findPackageRoot(startDir) {
|
|
5666
6052
|
let current = resolve8(startDir);
|
|
@@ -5675,7 +6061,7 @@ function findPackageRoot(startDir) {
|
|
|
5675
6061
|
} catch {
|
|
5676
6062
|
}
|
|
5677
6063
|
}
|
|
5678
|
-
const parent =
|
|
6064
|
+
const parent = dirname10(current);
|
|
5679
6065
|
if (parent === current) {
|
|
5680
6066
|
return resolve8(startDir);
|
|
5681
6067
|
}
|
|
@@ -5693,7 +6079,7 @@ function hasSkillDirectories(dir) {
|
|
|
5693
6079
|
return false;
|
|
5694
6080
|
}
|
|
5695
6081
|
}
|
|
5696
|
-
function resolveAgentQaSkillsDirectory(startDir =
|
|
6082
|
+
function resolveAgentQaSkillsDirectory(startDir = dirname10(fileURLToPath(import.meta.url))) {
|
|
5697
6083
|
const packageRoot = findPackageRoot(startDir);
|
|
5698
6084
|
const packagedSkills = join7(packageRoot, "skills");
|
|
5699
6085
|
if (hasSkillDirectories(packagedSkills)) {
|
|
@@ -5727,7 +6113,7 @@ function listAgentQaSkills(skillsDir) {
|
|
|
5727
6113
|
function createSkillsCommand(options = {}) {
|
|
5728
6114
|
const stdout = options.stdout ?? process.stdout;
|
|
5729
6115
|
const stderr = options.stderr ?? process.stderr;
|
|
5730
|
-
return new
|
|
6116
|
+
return new Command19("skills").description("List packaged agent-qa skills").option("--json", "print machine-readable output").action((opts) => {
|
|
5731
6117
|
const resolved = resolveAgentQaSkillsDirectory(options.startDir);
|
|
5732
6118
|
const skills = listAgentQaSkills(resolved.path);
|
|
5733
6119
|
if (resolved.source === "missing" || skills.length === 0) {
|
|
@@ -5808,7 +6194,7 @@ function rejectRemovedCliSurface(input3 = {}) {
|
|
|
5808
6194
|
|
|
5809
6195
|
// src/cli.ts
|
|
5810
6196
|
rejectRemovedCliSurface();
|
|
5811
|
-
var program = new
|
|
6197
|
+
var program = new Command20();
|
|
5812
6198
|
program.enablePositionalOptions();
|
|
5813
6199
|
program.name("agent-qa").description("AI-powered testing framework").version(VERSION);
|
|
5814
6200
|
program.option("--config <path>", "config file path", "agent-qa.config.yaml").option("--log-level <level>", "log verbosity: silent|error|warn|info|debug").option("--verbose", "shorthand for --log-level debug").option("--quiet", "shorthand for --log-level silent");
|
|
@@ -5825,6 +6211,7 @@ program.addCommand(createQueueCommand());
|
|
|
5825
6211
|
program.addCommand(createCacheCommand());
|
|
5826
6212
|
program.addCommand(createValidateCommand());
|
|
5827
6213
|
program.addCommand(createAuthCommand());
|
|
6214
|
+
program.addCommand(createAuthStateCommand());
|
|
5828
6215
|
program.addCommand(createDevicesCommand());
|
|
5829
6216
|
program.addCommand(createIdsCommand());
|
|
5830
6217
|
program.addCommand(createCreateTestCommand());
|