@treeseed/sdk 0.12.7 → 0.12.9
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.
|
@@ -57,6 +57,27 @@ function extractConfirmationUrl(body) {
|
|
|
57
57
|
const relative = body.match(/(?:\/auth\/confirm-email\?[^"' <>\n]+|\/team-invites\/[^"' <>\n]+\/accept)/u)?.[0];
|
|
58
58
|
return relative ?? null;
|
|
59
59
|
}
|
|
60
|
+
async function sleep(ms) {
|
|
61
|
+
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
62
|
+
}
|
|
63
|
+
function isRetryableNavigationError(error) {
|
|
64
|
+
const message = error instanceof Error ? error.message : String(error ?? "");
|
|
65
|
+
return /Timeout|ERR_CONNECTION|ECONNRESET|ECONNREFUSED|ETIMEDOUT|503|502|504/iu.test(message);
|
|
66
|
+
}
|
|
67
|
+
async function navigateScenePage(page, url) {
|
|
68
|
+
let lastError = null;
|
|
69
|
+
for (let attempt = 1; attempt <= 3; attempt += 1) {
|
|
70
|
+
try {
|
|
71
|
+
await page.goto(url, { waitUntil: "domcontentloaded", timeout: 45e3 });
|
|
72
|
+
return;
|
|
73
|
+
} catch (error) {
|
|
74
|
+
lastError = error;
|
|
75
|
+
if (!isRetryableNavigationError(error) || attempt >= 3) break;
|
|
76
|
+
await sleep(500 * attempt);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
throw lastError instanceof Error ? lastError : new Error(String(lastError ?? `Navigation failed for ${url}`));
|
|
80
|
+
}
|
|
60
81
|
async function assertionReport(kind, action, selector) {
|
|
61
82
|
const startedAt = /* @__PURE__ */ new Date();
|
|
62
83
|
try {
|
|
@@ -85,7 +106,7 @@ function createBuiltInTreeseedScenePlugins() {
|
|
|
85
106
|
summary: "Navigate to a route or absolute URL.",
|
|
86
107
|
async run({ action, context }) {
|
|
87
108
|
if (!("goto" in action)) return { ok: false, diagnostics: [sceneErrorDiagnostic("scene.invalid_action", "Expected goto action.", "workflow.action.goto")] };
|
|
88
|
-
await context.session.page
|
|
109
|
+
await navigateScenePage(context.session.page, context.resolveUrl(action.goto));
|
|
89
110
|
return { ok: true, diagnostics: [] };
|
|
90
111
|
}
|
|
91
112
|
},
|
|
@@ -219,18 +240,18 @@ function createBuiltInTreeseedScenePlugins() {
|
|
|
219
240
|
search.searchParams.set("query", `to:${email}`);
|
|
220
241
|
context.timeline.push("mailpit.inbox.open", { messageId: id, email, url: search.toString() }, step.id);
|
|
221
242
|
context.progress?.push("mailpit.inbox.open", { messageId: id, email }, { stepId: step.id });
|
|
222
|
-
await context.session.page
|
|
243
|
+
await navigateScenePage(context.session.page, search.toString());
|
|
223
244
|
await context.sleep(displayInboxSeconds * 1e3);
|
|
224
245
|
}
|
|
225
246
|
if (displayMessageSeconds && displayMessageSeconds > 0) {
|
|
226
247
|
const view = new URL(`view/${encodeURIComponent(id)}`, mailpitBase);
|
|
227
248
|
context.timeline.push("mailpit.message.open", { messageId: id, email, url: view.toString() }, step.id);
|
|
228
249
|
context.progress?.push("mailpit.message.open", { messageId: id, email }, { stepId: step.id });
|
|
229
|
-
await context.session.page
|
|
250
|
+
await navigateScenePage(context.session.page, view.toString());
|
|
230
251
|
await context.sleep(displayMessageSeconds * 1e3);
|
|
231
252
|
}
|
|
232
253
|
context.timeline.push("mailpit.confirm.open", { messageId: id, email, url: resolvedUrl }, step.id);
|
|
233
|
-
await context.session.page
|
|
254
|
+
await navigateScenePage(context.session.page, resolvedUrl);
|
|
234
255
|
return { ok: true, diagnostics: [] };
|
|
235
256
|
} catch (error) {
|
|
236
257
|
return { ok: false, diagnostics: [sceneErrorDiagnostic("scene.mailpit_unavailable", error instanceof Error ? error.message : String(error ?? "Mailpit confirmation failed."), `workflow.${step.id}.action.mailpitConfirmLatest`)] };
|
package/dist/scenes/types.d.ts
CHANGED
|
@@ -1809,7 +1809,10 @@ export type TreeseedSceneLocator = {
|
|
|
1809
1809
|
isVisible(): Promise<boolean>;
|
|
1810
1810
|
};
|
|
1811
1811
|
export type TreeseedScenePage = {
|
|
1812
|
-
goto(url: string
|
|
1812
|
+
goto(url: string, options?: {
|
|
1813
|
+
waitUntil?: 'load' | 'domcontentloaded' | 'networkidle';
|
|
1814
|
+
timeout?: number;
|
|
1815
|
+
}): Promise<void>;
|
|
1813
1816
|
url(): string;
|
|
1814
1817
|
locator(selector: string): TreeseedSceneLocator;
|
|
1815
1818
|
getByTestId(testId: string): TreeseedSceneLocator;
|
|
@@ -58,6 +58,22 @@ function serviceHeaders(input) {
|
|
|
58
58
|
"x-treeseed-service-secret": configuredValue(input, "TREESEED_API_WEB_SERVICE_SECRET") ?? configuredValue(input, "TREESEED_WEB_SERVICE_SECRET") ?? "treeseed-web-service-dev-secret"
|
|
59
59
|
};
|
|
60
60
|
}
|
|
61
|
+
async function sleep(ms) {
|
|
62
|
+
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
63
|
+
}
|
|
64
|
+
function retryDelayMs(attempt) {
|
|
65
|
+
return Math.min(500 * attempt, 2e3);
|
|
66
|
+
}
|
|
67
|
+
function isRetryableStatus(status) {
|
|
68
|
+
return [408, 425, 429, 500, 502, 503, 504].includes(status);
|
|
69
|
+
}
|
|
70
|
+
async function gotoSceneFixturePage(page, url) {
|
|
71
|
+
await page.goto(url, { waitUntil: "domcontentloaded", timeout: 45e3 });
|
|
72
|
+
await page.waitForLoadState?.("networkidle", { timeout: 5e3 }).catch(() => void 0);
|
|
73
|
+
}
|
|
74
|
+
function isSignInUrl(value) {
|
|
75
|
+
return /\/auth\/sign-in/u.test(value);
|
|
76
|
+
}
|
|
61
77
|
function seedActorsForRoles(roles) {
|
|
62
78
|
const actors = {};
|
|
63
79
|
for (const role of roles) {
|
|
@@ -76,32 +92,33 @@ function seedActorsForRoles(roles) {
|
|
|
76
92
|
async function seedVisualAuditFixtures(input) {
|
|
77
93
|
const actors = seedActorsForRoles(input.roles);
|
|
78
94
|
if (Object.keys(actors).length === 0) return [];
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
95
|
+
let lastFailure = null;
|
|
96
|
+
for (let attempt = 1; attempt <= 3; attempt += 1) {
|
|
97
|
+
try {
|
|
98
|
+
const response = await fetch(new URL("/v1/acceptance/seed", input.baseUrl).toString(), {
|
|
99
|
+
method: "POST",
|
|
100
|
+
headers: serviceHeaders(input),
|
|
101
|
+
body: JSON.stringify({
|
|
102
|
+
namespace: "visual-audit",
|
|
103
|
+
password: TREESEED_VISUAL_AUDIT_PASSWORD,
|
|
104
|
+
actors
|
|
105
|
+
})
|
|
106
|
+
});
|
|
107
|
+
const text = await response.text();
|
|
108
|
+
if (response.ok) return [];
|
|
109
|
+
lastFailure = `HTTP ${response.status}: ${text.slice(0, 500)}`;
|
|
110
|
+
if (!isRetryableStatus(response.status) || attempt >= 3) break;
|
|
111
|
+
} catch (error) {
|
|
112
|
+
lastFailure = error instanceof Error ? error.message : String(error ?? "local fixture API is unavailable");
|
|
113
|
+
if (attempt >= 3) break;
|
|
96
114
|
}
|
|
97
|
-
|
|
98
|
-
} catch (error) {
|
|
99
|
-
return [sceneWarningDiagnostic(
|
|
100
|
-
"scene.visual_audit_fixture_unavailable",
|
|
101
|
-
`Visual audit API fixture seed failed against ${input.baseUrl}: ${error instanceof Error ? error.message : String(error ?? "local fixture API is unavailable")}.`,
|
|
102
|
-
"roles"
|
|
103
|
-
)];
|
|
115
|
+
await sleep(retryDelayMs(attempt));
|
|
104
116
|
}
|
|
117
|
+
return [sceneWarningDiagnostic(
|
|
118
|
+
"scene.visual_audit_fixture_unavailable",
|
|
119
|
+
`Visual audit API fixture seed failed against ${input.baseUrl}: ${lastFailure ?? "local fixture API is unavailable"}.`,
|
|
120
|
+
"roles"
|
|
121
|
+
)];
|
|
105
122
|
}
|
|
106
123
|
async function ensureTreeseedSceneVisualAuditRoleFixtures(input) {
|
|
107
124
|
const diagnostics = [];
|
|
@@ -165,40 +182,54 @@ async function signInTreeseedSceneVisualAuditRole(input) {
|
|
|
165
182
|
return [sceneErrorDiagnostic("scene.visual_audit_role_unknown", `Visual audit role "${input.role}" has no fixture login.`, "role")];
|
|
166
183
|
}
|
|
167
184
|
const apiBaseUrl = input.apiBaseUrl?.trim() || input.baseUrl;
|
|
185
|
+
let lastError = null;
|
|
168
186
|
try {
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
187
|
+
for (let attempt = 1; attempt <= 3; attempt += 1) {
|
|
188
|
+
const session = await clientFor(apiBaseUrl).webSignIn({ login: user.email, password: user.password });
|
|
189
|
+
const accessToken = session.payload.accessToken;
|
|
190
|
+
if (accessToken) {
|
|
191
|
+
const webUrl = new URL(input.baseUrl);
|
|
192
|
+
await input.page.context().addCookies([{
|
|
193
|
+
name: "ts_market_api_access",
|
|
194
|
+
value: accessToken,
|
|
195
|
+
domain: webUrl.hostname,
|
|
196
|
+
path: "/",
|
|
197
|
+
httpOnly: true,
|
|
198
|
+
secure: webUrl.protocol === "https:",
|
|
199
|
+
sameSite: "Lax",
|
|
200
|
+
expires: Math.floor(Date.now() / 1e3) + Number(session.payload.expiresInSeconds ?? 900)
|
|
201
|
+
}]);
|
|
202
|
+
await gotoSceneFixturePage(input.page, new URL("/app/", input.baseUrl).toString());
|
|
203
|
+
if (!isSignInUrl(input.page.url())) return [];
|
|
204
|
+
}
|
|
205
|
+
await sleep(retryDelayMs(attempt));
|
|
185
206
|
}
|
|
186
207
|
} catch {
|
|
187
208
|
}
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
209
|
+
for (let attempt = 1; attempt <= 3; attempt += 1) {
|
|
210
|
+
try {
|
|
211
|
+
const signInUrl = new URL("/auth/sign-in", input.baseUrl);
|
|
212
|
+
signInUrl.searchParams.set("returnTo", "/app/");
|
|
213
|
+
await gotoSceneFixturePage(input.page, signInUrl.toString());
|
|
214
|
+
await input.page.locator('input[name="login"], input[name="email"], input[name="emailOrUsername"], input[name="username"]').first().fill(user.email, { timeout: 1e4 });
|
|
215
|
+
await input.page.locator('input[name="password"]').first().fill(user.password, { timeout: 1e4 });
|
|
216
|
+
await input.page.getByRole("button", { name: /sign in/i }).click({ timeout: 1e4 });
|
|
217
|
+
await input.page.waitForURL?.((url) => !isSignInUrl(url.pathname), { timeout: 15e3 }).catch(() => void 0);
|
|
218
|
+
await input.page.waitForLoadState?.("networkidle", { timeout: 5e3 }).catch(() => void 0);
|
|
219
|
+
if (!isSignInUrl(input.page.url())) {
|
|
220
|
+
return [];
|
|
221
|
+
}
|
|
222
|
+
lastError = new Error(`Visual audit login for ${input.role} did not leave the sign-in page. Ensure deterministic fixture user ${user.email} exists.`);
|
|
223
|
+
} catch (error) {
|
|
224
|
+
lastError = error;
|
|
197
225
|
}
|
|
198
|
-
|
|
199
|
-
} catch (error) {
|
|
200
|
-
return [sceneWarningDiagnostic("scene.visual_audit_role_login_failed", error instanceof Error ? error.message : String(error ?? `Visual audit login for ${input.role} failed.`), "roles")];
|
|
226
|
+
await sleep(retryDelayMs(attempt));
|
|
201
227
|
}
|
|
228
|
+
return [sceneWarningDiagnostic(
|
|
229
|
+
"scene.visual_audit_role_login_failed",
|
|
230
|
+
lastError instanceof Error ? lastError.message : String(lastError ?? `Visual audit login for ${input.role} failed.`),
|
|
231
|
+
"roles"
|
|
232
|
+
)];
|
|
202
233
|
}
|
|
203
234
|
export {
|
|
204
235
|
TREESEED_VISUAL_AUDIT_PASSWORD,
|
|
@@ -1048,9 +1048,11 @@ export declare function workflowRelease(helpers: WorkflowOperationHelpers, input
|
|
|
1048
1048
|
rootInstall: {
|
|
1049
1049
|
status: string;
|
|
1050
1050
|
reason: string;
|
|
1051
|
+
attempts?: undefined;
|
|
1051
1052
|
} | {
|
|
1052
1053
|
status: string;
|
|
1053
1054
|
reason: null;
|
|
1055
|
+
attempts: number;
|
|
1054
1056
|
};
|
|
1055
1057
|
commit: {
|
|
1056
1058
|
committed: boolean;
|
|
@@ -1933,31 +1933,36 @@ function runReleaseNpmInstall(repoDir, options = {}) {
|
|
|
1933
1933
|
}
|
|
1934
1934
|
const args = repoDir === options.workspaceRoot ? ["install", "--package-lock-only", "--ignore-scripts", "--no-audit", "--no-fund"] : ["install", "--package-lock-only", "--ignore-scripts", "--workspaces=false", "--no-audit", "--no-fund"];
|
|
1935
1935
|
const spawnCommand = npmCommandForWorkflowSpawn(args);
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1936
|
+
let lastDetail = "";
|
|
1937
|
+
for (let attempt = 1; attempt <= 10; attempt += 1) {
|
|
1938
|
+
const result = spawnSync(spawnCommand.command, spawnCommand.args, {
|
|
1939
|
+
cwd: repoDir,
|
|
1940
|
+
env: {
|
|
1941
|
+
...process.env,
|
|
1942
|
+
npm_config_audit: "false",
|
|
1943
|
+
npm_config_fetch_retries: "4",
|
|
1944
|
+
npm_config_fund: "false",
|
|
1945
|
+
npm_config_foreground_scripts: "true",
|
|
1946
|
+
npm_config_loglevel: "warn",
|
|
1947
|
+
npm_config_maxsockets: "4",
|
|
1948
|
+
npm_config_prefer_online: "true",
|
|
1949
|
+
npm_config_progress: "false"
|
|
1950
|
+
},
|
|
1951
|
+
stdio: "pipe",
|
|
1952
|
+
encoding: "utf8"
|
|
1953
|
+
});
|
|
1954
|
+
if (result.status === 0) {
|
|
1955
|
+
return { status: "completed", reason: null, attempts: attempt };
|
|
1956
|
+
}
|
|
1957
|
+
lastDetail = [
|
|
1954
1958
|
result.error?.message,
|
|
1955
1959
|
result.stderr?.trim(),
|
|
1956
1960
|
result.stdout?.trim()
|
|
1957
1961
|
].filter(Boolean).join("\n");
|
|
1958
|
-
|
|
1962
|
+
if (!/No matching version found|notarget|ETARGET|E404/u.test(lastDetail) || attempt === 10) break;
|
|
1963
|
+
spawnSync("sleep", ["30"], { stdio: "ignore" });
|
|
1959
1964
|
}
|
|
1960
|
-
|
|
1965
|
+
throw new Error(lastDetail || `npm ${args.join(" ")} failed`);
|
|
1961
1966
|
}
|
|
1962
1967
|
function pathIsWithin(parent, candidate) {
|
|
1963
1968
|
const path = relative(parent, candidate);
|