diff --git a/.github/actions/ensure-base-commit/action.yml b/.github/actions/ensure-base-commit/action.yml index ec05a4d64159..85d41bd6106d 100644 --- a/.github/actions/ensure-base-commit/action.yml +++ b/.github/actions/ensure-base-commit/action.yml @@ -44,6 +44,15 @@ runs: fetch "$@" } + echo "Base commit missing; fetching exact SHA before branch history." + if ! fetch_base_ref --no-tags --depth=1 origin "$BASE_SHA"; then + echo "::warning title=ensure-base-commit exact fetch failed::Failed to fetch exact base SHA $BASE_SHA" + fi + if git rev-parse --verify "$BASE_SHA^{commit}" >/dev/null 2>&1; then + echo "Resolved base commit after exact fetch: $BASE_SHA" + exit 0 + fi + for deepen_by in 25 100 300; do echo "Base commit missing; deepening $FETCH_REF by $deepen_by." if ! fetch_base_ref --no-tags --deepen="$deepen_by" origin -- "$FETCH_REF"; then @@ -64,4 +73,5 @@ runs: exit 0 fi - echo "Base commit still unavailable after fetch attempts: $BASE_SHA" + echo "::error title=ensure-base-commit missing base::Base commit still unavailable after fetch attempts: $BASE_SHA" + exit 1 diff --git a/extensions/msteams/src/file-consent.test.ts b/extensions/msteams/src/file-consent.test.ts index e8753534536a..ab0f970cec9c 100644 --- a/extensions/msteams/src/file-consent.test.ts +++ b/extensions/msteams/src/file-consent.test.ts @@ -1,6 +1,7 @@ // Msteams tests cover file consent plugin behavior. import { describe, expect, it, vi } from "vitest"; import { uploadToConsentUrl } from "./file-consent.js"; +import { resolveMSTeamsSharePointUploadTimeoutMs } from "./request-timeout.js"; import { buildUserAgent } from "./user-agent.js"; // Helper: a resolveFn that returns a public IP by default @@ -321,6 +322,132 @@ describe("uploadToConsentUrl", () => { expect(opts?.body).toEqual(new Uint8Array(Buffer.from("hello"))); }); + it("aborts consent uploads that do not finish before the request timeout", async () => { + vi.useFakeTimers(); + try { + let observedSignal: AbortSignal | undefined; + const fetchFn = vi.fn( + async (_url, init) => + await new Promise((_resolve, reject) => { + observedSignal = init?.signal ?? undefined; + observedSignal?.addEventListener( + "abort", + () => { + const reason = observedSignal?.reason; + reject(reason instanceof Error ? reason : new Error("request aborted")); + }, + { once: true }, + ); + }), + ); + + const uploadPromise = uploadToConsentUrl({ + url: "https://contoso.sharepoint.com/upload", + buffer: Buffer.from("hello"), + fetchFn, + timeoutMs: 25, + validationOpts: { resolveFn: publicResolve }, + }); + + await vi.advanceTimersByTimeAsync(0); + expect(fetchFn).toHaveBeenCalledOnce(); + expect(observedSignal?.aborted).toBe(false); + const uploadRejection = expect(uploadPromise).rejects.toMatchObject({ + name: "TimeoutError", + message: "request timed out", + }); + + await vi.advanceTimersByTimeAsync(25); + await uploadRejection; + expect(observedSignal?.aborted).toBe(true); + } finally { + vi.useRealTimers(); + } + }); + + it("allows consent uploads that complete before the request timeout", async () => { + vi.useFakeTimers(); + try { + let observedSignal: AbortSignal | undefined; + const fetchFn = vi.fn( + async (_url, init) => + await new Promise((resolve, reject) => { + observedSignal = init?.signal ?? undefined; + observedSignal?.addEventListener( + "abort", + () => reject(new DOMException("consent upload timed out", "AbortError")), + { once: true }, + ); + setTimeout(() => resolve(new Response(null, { status: 200 })), 25); + }), + ); + + const uploadPromise = uploadToConsentUrl({ + url: "https://contoso.sharepoint.com/upload", + buffer: Buffer.from("hello"), + fetchFn, + timeoutMs: 50, + validationOpts: { resolveFn: publicResolve }, + }); + + await vi.advanceTimersByTimeAsync(0); + expect(fetchFn).toHaveBeenCalledOnce(); + expect(observedSignal?.aborted).toBe(false); + + await vi.advanceTimersByTimeAsync(25); + await expect(uploadPromise).resolves.toBeUndefined(); + expect(observedSignal?.aborted).toBe(false); + + await vi.advanceTimersByTimeAsync(25); + expect(observedSignal?.aborted).toBe(false); + } finally { + vi.useRealTimers(); + } + }); + + it("allows size-budgeted consent uploads that complete after the base upload deadline", async () => { + vi.useFakeTimers(); + try { + const buffer = Buffer.alloc(512 * 1024); + const resolvedTimeoutMs = resolveMSTeamsSharePointUploadTimeoutMs(buffer.length); + const baseTimeoutMs = resolveMSTeamsSharePointUploadTimeoutMs(0); + const completionMs = baseTimeoutMs + 1_000; + let observedSignal: AbortSignal | undefined; + const fetchFn = vi.fn( + async (_url, init) => + await new Promise((resolve, reject) => { + observedSignal = init?.signal ?? undefined; + observedSignal?.addEventListener( + "abort", + () => reject(new DOMException("consent upload timed out", "AbortError")), + { once: true }, + ); + setTimeout(() => resolve(new Response(null, { status: 200 })), completionMs); + }), + ); + + expect(completionMs).toBeGreaterThan(baseTimeoutMs); + expect(completionMs).toBeLessThan(resolvedTimeoutMs); + + const uploadPromise = uploadToConsentUrl({ + url: "https://contoso.sharepoint.com/upload", + buffer, + fetchFn, + validationOpts: { resolveFn: publicResolve }, + }); + + await vi.advanceTimersByTimeAsync(0); + expect(fetchFn).toHaveBeenCalledOnce(); + expect(observedSignal?.aborted).toBe(false); + + await vi.advanceTimersByTimeAsync(completionMs); + await expect(uploadPromise).resolves.toBeUndefined(); + expect(observedSignal?.aborted).toBe(false); + } finally { + vi.useRealTimers(); + } + }); + it("blocks upload to a disallowed host", async () => { const mockFetch = vi.fn(); await expect( @@ -364,15 +491,15 @@ describe("uploadToConsentUrl", () => { expect(mockFetch).toHaveBeenCalledOnce(); const [url, opts] = firstFetchCall(mockFetch); expect(url).toBe("https://contoso.sharepoint.com/sites/uploads/file.pdf"); - expect(opts).toEqual({ - method: "PUT", - headers: { - "User-Agent": buildUserAgent(), - "Content-Type": "application/pdf", - "Content-Range": "bytes 0-11/12", - }, - body: new Uint8Array(buffer), + expect(opts?.method).toBe("PUT"); + expect(opts?.headers).toEqual({ + "User-Agent": buildUserAgent(), + "Content-Type": "application/pdf", + "Content-Range": "bytes 0-11/12", }); + expect(opts?.body).toEqual(new Uint8Array(buffer)); + expect(opts?.signal).toBeInstanceOf(AbortSignal); + expect(opts?.signal?.aborted).toBe(false); }); it("throws on non-OK response after passing validation", async () => { diff --git a/extensions/msteams/src/file-consent.ts b/extensions/msteams/src/file-consent.ts index e740e7583bdb..14a70de8b09a 100644 --- a/extensions/msteams/src/file-consent.ts +++ b/extensions/msteams/src/file-consent.ts @@ -11,6 +11,8 @@ import { lookup } from "node:dns/promises"; import { isPrivateIpAddress } from "openclaw/plugin-sdk/ssrf-policy"; import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/string-coerce-runtime"; +import { fetchWithTimeout } from "openclaw/plugin-sdk/text-utility-runtime"; +import { resolveMSTeamsSharePointUploadTimeoutMs } from "./request-timeout.js"; import { buildUserAgent } from "./user-agent.js"; /** @@ -198,6 +200,7 @@ export async function uploadToConsentUrl(params: { buffer: Buffer; contentType?: string; fetchFn?: typeof fetch; + timeoutMs?: number; /** Override for testing — custom allowlist and DNS resolver */ validationOpts?: { allowlist?: readonly string[]; @@ -207,15 +210,20 @@ export async function uploadToConsentUrl(params: { await validateConsentUploadUrl(params.url, params.validationOpts); const fetchFn = params.fetchFn ?? fetch; - const res = await fetchFn(params.url, { - method: "PUT", - headers: { - "User-Agent": buildUserAgent(), - "Content-Type": params.contentType ?? "application/octet-stream", - "Content-Range": `bytes 0-${params.buffer.length - 1}/${params.buffer.length}`, + const res = await fetchWithTimeout( + params.url, + { + method: "PUT", + headers: { + "User-Agent": buildUserAgent(), + "Content-Type": params.contentType ?? "application/octet-stream", + "Content-Range": `bytes 0-${params.buffer.length - 1}/${params.buffer.length}`, + }, + body: new Uint8Array(params.buffer), }, - body: new Uint8Array(params.buffer), - }); + params.timeoutMs ?? resolveMSTeamsSharePointUploadTimeoutMs(params.buffer.length), + fetchFn, + ); if (!res.ok) { throw new Error(`File upload to consent URL failed: ${res.status} ${res.statusText}`); diff --git a/extensions/msteams/src/request-timeout.test.ts b/extensions/msteams/src/request-timeout.test.ts index a17b6f7d4e62..77a205ff5c7c 100644 --- a/extensions/msteams/src/request-timeout.test.ts +++ b/extensions/msteams/src/request-timeout.test.ts @@ -1,6 +1,9 @@ // Msteams tests cover shared inbound request deadlines. import { describe, expect, it, vi } from "vitest"; -import { withMSTeamsRequestDeadline } from "./request-timeout.js"; +import { + resolveMSTeamsSharePointUploadTimeoutMs, + withMSTeamsRequestDeadline, +} from "./request-timeout.js"; describe("withMSTeamsRequestDeadline", () => { it("does not start work after the operation deadline has expired", async () => { @@ -21,3 +24,12 @@ describe("withMSTeamsRequestDeadline", () => { expect(work).not.toHaveBeenCalled(); }); }); + +describe("resolveMSTeamsSharePointUploadTimeoutMs", () => { + it("adds transfer budget to the base file-consent upload deadline", () => { + const oneHundredMiB = 100 * 1024 * 1024; + + expect(resolveMSTeamsSharePointUploadTimeoutMs(0)).toBe(300_000); + expect(resolveMSTeamsSharePointUploadTimeoutMs(oneHundredMiB)).toBe(700_000); + }); +}); diff --git a/extensions/msteams/src/request-timeout.ts b/extensions/msteams/src/request-timeout.ts index ac2cc534238b..e3fcb15f4533 100644 --- a/extensions/msteams/src/request-timeout.ts +++ b/extensions/msteams/src/request-timeout.ts @@ -1,4 +1,5 @@ // Msteams plugin module implements request deadline behavior. +import { resolveTimerTimeoutMs } from "openclaw/plugin-sdk/number-runtime"; import { createProviderOperationDeadline, resolveProviderOperationTimeoutMs, @@ -7,6 +8,10 @@ import { import { withTimeout } from "openclaw/plugin-sdk/text-utility-runtime"; export const MSTEAMS_REQUEST_TIMEOUT_MS = 30_000; +// File-consent PUTs are data-plane transfers: keep a base stall bound, then +// add slow-transfer budget so valid large uploads do not hit a fixed cutoff. +const MSTEAMS_SHAREPOINT_UPLOAD_BASE_TIMEOUT_MS = 5 * 60_000; +const MSTEAMS_SHAREPOINT_UPLOAD_MIN_BYTES_PER_SECOND = 256 * 1024; // Cap optional enrichment before agent dispatch. The Teams SDK still holds the // webhook open for the agent turn, so this budget alone cannot prevent retries. @@ -39,3 +44,15 @@ export async function withMSTeamsRequestDeadline(params: { const timeoutMs = resolveMSTeamsRequestTimeoutMs(params.deadline); return await withTimeout(params.work(), timeoutMs, params.label); } + +export function resolveMSTeamsSharePointUploadTimeoutMs(sizeInBytes: number): number { + const bytes = Number.isFinite(sizeInBytes) && sizeInBytes > 0 ? Math.ceil(sizeInBytes) : 0; + const transferBudgetMs = Math.ceil( + (bytes / MSTEAMS_SHAREPOINT_UPLOAD_MIN_BYTES_PER_SECOND) * 1000, + ); + return resolveTimerTimeoutMs( + MSTEAMS_SHAREPOINT_UPLOAD_BASE_TIMEOUT_MS + transferBudgetMs, + MSTEAMS_SHAREPOINT_UPLOAD_BASE_TIMEOUT_MS, + 1, + ); +} diff --git a/scripts/audit-seams.mjs b/scripts/audit-seams.mjs index aa74b2a2864f..2416da640d04 100644 --- a/scripts/audit-seams.mjs +++ b/scripts/audit-seams.mjs @@ -9,8 +9,10 @@ import { BUNDLED_PLUGIN_PATH_PREFIX, BUNDLED_PLUGIN_ROOT_DIR, } from "./lib/bundled-plugin-paths.mjs"; +import { visitModuleSpecifiers } from "./lib/guard-inventory-utils.mjs"; import { optionalBundledClusterSet } from "./lib/optional-bundled-clusters.mjs"; import { escapeRegExp } from "./lib/regexp.mjs"; +import { toLine } from "./lib/ts-guard-utils.mjs"; const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); const srcRoot = path.join(repoRoot, "src"); @@ -157,10 +159,6 @@ async function walkAllCodeFiles(rootDir, options = {}) { return out.toSorted((left, right) => normalizePath(left).localeCompare(normalizePath(right))); } -function toLine(sourceFile, node) { - return sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile)).line + 1; -} - function resolveRelativeSpecifier(specifier, importerFile) { if (!specifier.startsWith(".")) { return null; @@ -213,27 +211,9 @@ function collectPluginSdkImports(filePath, sourceFile) { }); } - function visit(node) { - if (ts.isImportDeclaration(node) && ts.isStringLiteral(node.moduleSpecifier)) { - push("import", node.moduleSpecifier, node.moduleSpecifier.text); - } else if ( - ts.isExportDeclaration(node) && - node.moduleSpecifier && - ts.isStringLiteral(node.moduleSpecifier) - ) { - push("export", node.moduleSpecifier, node.moduleSpecifier.text); - } else if ( - ts.isCallExpression(node) && - node.expression.kind === ts.SyntaxKind.ImportKeyword && - node.arguments.length === 1 && - ts.isStringLiteral(node.arguments[0]) - ) { - push("dynamic-import", node.arguments[0], node.arguments[0].text); - } - ts.forEachChild(node, visit); - } - - visit(sourceFile); + visitModuleSpecifiers(ts, sourceFile, ({ kind, specifierNode, specifier }) => { + push(kind, specifierNode, specifier); + }); return entries; } @@ -245,15 +225,7 @@ async function collectCorePluginSdkImports() { continue; } const source = await fs.readFile(filePath, "utf8"); - const scriptKind = - filePath.endsWith(".tsx") || filePath.endsWith(".jsx") ? ts.ScriptKind.TSX : ts.ScriptKind.TS; - const sourceFile = ts.createSourceFile( - filePath, - source, - ts.ScriptTarget.Latest, - true, - scriptKind, - ); + const sourceFile = ts.createSourceFile(filePath, source, ts.ScriptTarget.Latest, true); inventory.push(...collectPluginSdkImports(filePath, sourceFile)); } return inventory.toSorted(compareImports); @@ -284,20 +256,11 @@ function collectOptionalClusterStaticImports(filePath, sourceFile) { }); } - function visit(node) { - if (ts.isImportDeclaration(node) && ts.isStringLiteral(node.moduleSpecifier)) { - push("import", node.moduleSpecifier, node.moduleSpecifier.text); - } else if ( - ts.isExportDeclaration(node) && - node.moduleSpecifier && - ts.isStringLiteral(node.moduleSpecifier) - ) { - push("export", node.moduleSpecifier, node.moduleSpecifier.text); + visitModuleSpecifiers(ts, sourceFile, ({ kind, specifierNode, specifier }) => { + if (kind !== "dynamic-import") { + push(kind, specifierNode, specifier); } - ts.forEachChild(node, visit); - } - - visit(sourceFile); + }); return entries; } @@ -310,15 +273,7 @@ async function collectOptionalClusterStaticLeaks() { continue; } const source = await fs.readFile(filePath, "utf8"); - const scriptKind = - filePath.endsWith(".tsx") || filePath.endsWith(".jsx") ? ts.ScriptKind.TSX : ts.ScriptKind.TS; - const sourceFile = ts.createSourceFile( - filePath, - source, - ts.ScriptTarget.Latest, - true, - scriptKind, - ); + const sourceFile = ts.createSourceFile(filePath, source, ts.ScriptTarget.Latest, true); inventory.push(...collectOptionalClusterStaticImports(filePath, sourceFile)); } return inventory.toSorted((left, right) => { diff --git a/scripts/check-dynamic-import-warts.mjs b/scripts/check-dynamic-import-warts.mjs index bc42edfd6dce..35adcab82e9a 100644 --- a/scripts/check-dynamic-import-warts.mjs +++ b/scripts/check-dynamic-import-warts.mjs @@ -14,64 +14,44 @@ import { const repoRoot = resolveRepoRoot(import.meta.url); const defaultRoots = [path.join(repoRoot, "src"), path.join(repoRoot, "extensions")]; -function readStringLiteral(node) { - if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node)) { - return node.text; - } - return null; -} - function isTypeOnlyImportDeclaration(node) { const clause = node.importClause; - if (!clause) { - return false; - } - if (clause.isTypeOnly) { - return true; - } - if (clause.name) { - return false; - } - const bindings = clause.namedBindings; - return ( - Boolean(bindings) && - ts.isNamedImports(bindings) && - bindings.elements.length > 0 && - bindings.elements.every((element) => element.isTypeOnly) + return Boolean( + clause && + (ts.isTypeOnlyImportDeclaration(clause) || + (!clause.name && + ts.isNamedImports(clause.namedBindings) && + clause.namedBindings.elements.length > 0 && + clause.namedBindings.elements.every(ts.isTypeOnlyImportOrExportDeclaration))), ); } function isTypeOnlyExportDeclaration(node) { - if (node.isTypeOnly === true) { - return true; - } const clause = node.exportClause; return ( - Boolean(clause) && - ts.isNamedExports(clause) && - clause.elements.length > 0 && - clause.elements.every((element) => element.isTypeOnly) + node.isTypeOnly === true || + Boolean( + clause && + ts.isNamedExports(clause) && + clause.elements.length > 0 && + clause.elements.every(ts.isTypeOnlyImportOrExportDeclaration), + ) ); } -function readDeclarationName(node) { +function isExecuteDeclaration(node) { if ( - (ts.isFunctionDeclaration(node) || - ts.isMethodDeclaration(node) || - ts.isVariableDeclaration(node)) && - node.name && - ts.isIdentifier(node.name) + !ts.isFunctionDeclaration(node) && + !ts.isMethodDeclaration(node) && + !ts.isVariableDeclaration(node) && + !ts.isPropertyAssignment(node) ) { - return node.name.text; + return false; } - - if (ts.isPropertyAssignment(node)) { - if (ts.isIdentifier(node.name) || ts.isStringLiteral(node.name)) { - return node.name.text; - } - } - - return null; + const name = ts.getNameOfDeclaration(node); + return Boolean( + name && (ts.isIdentifier(name) || ts.isStringLiteral(name)) && name.text === "execute", + ); } function isIgnoredTestHelperContent(content) { @@ -99,7 +79,6 @@ export function findDynamicImportAdvisories(content, fileName = "source.ts") { const staticRuntimeImports = new Map(); const dynamicImports = new Map(); const directExecuteImports = []; - const declarationStack = []; const addLine = (map, specifier, line) => { const lines = map.get(specifier) ?? []; @@ -108,11 +87,6 @@ export function findDynamicImportAdvisories(content, fileName = "source.ts") { }; const visit = (node) => { - const declarationName = readDeclarationName(node); - if (declarationName) { - declarationStack.push(declarationName); - } - if ( ts.isImportDeclaration(node) && ts.isStringLiteral(node.moduleSpecifier) && @@ -135,11 +109,11 @@ export function findDynamicImportAdvisories(content, fileName = "source.ts") { node.expression.kind === ts.SyntaxKind.ImportKeyword && node.arguments.length > 0 ) { - const specifier = readStringLiteral(node.arguments[0]); + const specifier = ts.isStringLiteralLike(node.arguments[0]) ? node.arguments[0].text : null; if (specifier) { const line = toLine(sourceFile, node); addLine(dynamicImports, specifier, line); - if (declarationStack.includes("execute")) { + if (ts.findAncestor(node, isExecuteDeclaration)) { directExecuteImports.push({ line, reason: `direct dynamic import of "${specifier}" inside execute path; move it behind a cached loader`, @@ -149,9 +123,6 @@ export function findDynamicImportAdvisories(content, fileName = "source.ts") { } ts.forEachChild(node, visit); - if (declarationName) { - declarationStack.pop(); - } }; visit(sourceFile); diff --git a/scripts/check-no-raw-window-open.mjs b/scripts/check-no-raw-window-open.mjs index 5f685796724b..8f1239a4e8bf 100644 --- a/scripts/check-no-raw-window-open.mjs +++ b/scripts/check-no-raw-window-open.mjs @@ -16,19 +16,9 @@ const repoRoot = resolveRepoRoot(import.meta.url); const uiSourceDir = path.join(repoRoot, "ui", "src", "ui"); const allowedCallsites = new Set([path.join(uiSourceDir, "open-external-url.ts")]); -function asPropertyAccess(expression) { - if (ts.isPropertyAccessExpression(expression)) { - return expression; - } - if (typeof ts.isPropertyAccessChain === "function" && ts.isPropertyAccessChain(expression)) { - return expression; - } - return null; -} - function isRawWindowOpenCall(expression) { - const propertyAccess = asPropertyAccess(unwrapExpression(expression)); - if (!propertyAccess || propertyAccess.name.text !== "open") { + const propertyAccess = unwrapExpression(expression); + if (!ts.isPropertyAccessExpression(propertyAccess) || propertyAccess.name.text !== "open") { return false; } diff --git a/scripts/check-plugin-sdk-subpath-exports.mjs b/scripts/check-plugin-sdk-subpath-exports.mjs index fe359c3e2e5e..e22ae5dd7734 100644 --- a/scripts/check-plugin-sdk-subpath-exports.mjs +++ b/scripts/check-plugin-sdk-subpath-exports.mjs @@ -71,13 +71,7 @@ async function collectViolations() { for (const filePath of files) { const sourceText = readFileSync(filePath, "utf8"); - const sourceFile = ts.createSourceFile( - filePath, - sourceText, - ts.ScriptTarget.Latest, - true, - filePath.endsWith(".tsx") ? ts.ScriptKind.TSX : ts.ScriptKind.TS, - ); + const sourceFile = ts.createSourceFile(filePath, sourceText, ts.ScriptTarget.Latest, true); function push(kind, specifierNode, specifier) { const subpath = parsePluginSdkSubpath(specifier); diff --git a/scripts/lib/control-ui-i18n-raw-copy.ts b/scripts/lib/control-ui-i18n-raw-copy.ts index 97f2b01b7d7e..32689c893918 100644 --- a/scripts/lib/control-ui-i18n-raw-copy.ts +++ b/scripts/lib/control-ui-i18n-raw-copy.ts @@ -49,16 +49,6 @@ function normalizeRawCopyText(raw: string): string { .trim(); } -function lineNumberForOffset(source: string, offset: number): number { - let line = 1; - for (let index = 0; index < offset && index < source.length; index += 1) { - if (source.charCodeAt(index) === 10) { - line += 1; - } - } - return line; -} - function parseDoubleQuotedString(raw: string): string { try { return JSON.parse(`"${raw}"`) as string; @@ -118,6 +108,7 @@ export function collectControlUiRawCopyFromSource(params: { const { filePath, source, sourceFile } = params; const repoPath = toRepoPath(filePath); const findings: RawCopyFinding[] = []; + const toLine = (offset: number) => sourceFile.getLineAndCharacterOfPosition(offset).line + 1; const staticAttrPattern = /\b(aria-label|placeholder|title)\s*=\s*"((?:(?!\$\{)[^"\\]|\\.)*?\p{L}(?:(?!\$\{)[^"\\]|\\.)*?)"/gu; for (const match of source.matchAll(staticAttrPattern)) { @@ -125,7 +116,7 @@ export function collectControlUiRawCopyFromSource(params: { if (rawText) { pushRawCopyFinding(findings, { kind: "html-attribute", - line: lineNumberForOffset(source, match.index ?? 0), + line: toLine(match.index ?? 0), name: match[1] ?? "attribute", path: repoPath, text: parseDoubleQuotedString(rawText), @@ -140,7 +131,7 @@ export function collectControlUiRawCopyFromSource(params: { if (rawText) { pushRawCopyFinding(findings, { kind: "object-property", - line: lineNumberForOffset(source, match.index ?? 0), + line: toLine(match.index ?? 0), name: match[1] ?? "property", path: repoPath, text: parseDoubleQuotedString(rawText), @@ -162,7 +153,7 @@ export function collectControlUiRawCopyFromSource(params: { ...node.template.templateSpans.map((span) => span.literal.text), ].join(INTERPOLATION_MARKER); } - const line = lineNumberForOffset(source, node.template.getStart(sourceFile)); + const line = toLine(node.template.getStart(sourceFile)); for (const match of logicalText.matchAll(attrPattern)) { const rawText = match[2]; if (rawText?.includes(INTERPOLATION_MARKER)) { @@ -199,13 +190,7 @@ async function collectFindings(): Promise { const findings: RawCopyFinding[] = []; for (const filePath of files.toSorted((left, right) => left.localeCompare(right))) { const source = await readFile(filePath, "utf8"); - const sourceFile = ts.createSourceFile( - filePath, - source, - ts.ScriptTarget.Latest, - true, - filePath.endsWith(".tsx") ? ts.ScriptKind.TSX : ts.ScriptKind.TS, - ); + const sourceFile = ts.createSourceFile(filePath, source, ts.ScriptTarget.Latest, true); findings.push(...collectControlUiRawCopyFromSource({ filePath, source, sourceFile })); } return findings; diff --git a/scripts/lib/guard-inventory-utils.mjs b/scripts/lib/guard-inventory-utils.mjs index bfeb365baaf7..25b4fd9cf335 100644 --- a/scripts/lib/guard-inventory-utils.mjs +++ b/scripts/lib/guard-inventory-utils.mjs @@ -239,10 +239,9 @@ export function formatGroupedInventoryHuman(params, inventory) { /** Parse TypeScript files and collect sorted inventory entries from each source file. */ export async function collectTypeScriptInventory(params) { const inventory = []; - const scriptKind = params.scriptKind ?? params.ts.ScriptKind.TS; for (const filePath of params.files) { - const cacheKey = `${scriptKind}:${filePath}`; + const cacheKey = `${params.scriptKind ?? "auto"}:${filePath}`; let sourceFile = parsedTypeScriptSourceCache.get(cacheKey); if (!sourceFile) { let source = sourceTextCache.get(filePath); @@ -258,7 +257,7 @@ export async function collectTypeScriptInventory(params) { source, params.ts.ScriptTarget.Latest, true, - scriptKind, + params.scriptKind, ); parsedTypeScriptSourceCache.set(cacheKey, sourceFile); } diff --git a/scripts/report-test-temp-creations.mjs b/scripts/report-test-temp-creations.mjs index 1c5cea9b4737..b4527bf42f5a 100644 --- a/scripts/report-test-temp-creations.mjs +++ b/scripts/report-test-temp-creations.mjs @@ -200,27 +200,8 @@ function isTempDirHelperImportSpec(filePath, specifier) { return stripKnownExtension(resolvedPath) === stripKnownExtension(TEMP_DIR_HELPER_PATH); } -function scriptKindForFile(filePath) { - if (/\.[cm]?tsx$/u.test(filePath)) { - return ts.ScriptKind.TSX; - } - if (/\.[cm]?jsx$/u.test(filePath)) { - return ts.ScriptKind.JSX; - } - if (/\.[cm]?js$/u.test(filePath)) { - return ts.ScriptKind.JS; - } - return ts.ScriptKind.TS; -} - function createSourceFile(filePath, sourceText) { - return ts.createSourceFile( - filePath, - sourceText, - ts.ScriptTarget.Latest, - true, - scriptKindForFile(filePath), - ); + return ts.createSourceFile(filePath, sourceText, ts.ScriptTarget.Latest, true); } function lineForNode(sourceFile, node) { diff --git a/test/scripts/ci-workflow-guards.test.ts b/test/scripts/ci-workflow-guards.test.ts index 75a6a80575ca..a32ba131535c 100644 --- a/test/scripts/ci-workflow-guards.test.ts +++ b/test/scripts/ci-workflow-guards.test.ts @@ -1619,11 +1619,16 @@ describe("ci workflow guards", () => { it("bounds shared base commit fetches", () => { const action = readFileSync(".github/actions/ensure-base-commit/action.yml", "utf8"); + const exactFetch = action.indexOf('fetch_base_ref --no-tags --depth=1 origin "$BASE_SHA"'); + const branchDeepening = action.indexOf("for deepen_by in 25 100 300"); expect(action).toContain("fetch_base_ref()"); expect(action).toContain("timeout --signal=TERM --kill-after=10s 30s git"); expect(action).toContain("-c protocol.version=2"); expect(action).not.toContain("if ! git fetch --no-tags"); + expect(exactFetch).toBeGreaterThan(-1); + expect(branchDeepening).toBeGreaterThan(exactFetch); + expect(action).toContain("::error title=ensure-base-commit missing base::"); }); it("bounds early unauthenticated checkout fetches", () => {