Merge remote-tracking branch 'origin/main' into pr-102197-prep

This commit is contained in:
Jesse Merhi
2026-07-15 03:23:20 +10:00
13 changed files with 245 additions and 191 deletions

View File

@@ -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

View File

@@ -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<typeof fetch>(
async (_url, init) =>
await new Promise<Response>((_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<typeof fetch>(
async (_url, init) =>
await new Promise<Response>((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<typeof fetch>(
async (_url, init) =>
await new Promise<Response>((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 () => {

View File

@@ -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}`);

View File

@@ -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);
});
});

View File

@@ -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<T>(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,
);
}

View File

@@ -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) => {

View File

@@ -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);

View File

@@ -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;
}

View File

@@ -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);

View File

@@ -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<RawCopyFinding[]> {
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;

View File

@@ -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);
}

View File

@@ -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) {

View File

@@ -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", () => {