refactor(scripts): dedupe guard checks and smoke helpers

This commit is contained in:
Peter Steinberger
2026-03-02 08:51:27 +00:00
parent 5d53b61d9e
commit 00a2456b72
9 changed files with 344 additions and 576 deletions

View File

@@ -2,10 +2,16 @@
import { promises as fs } from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import ts from "typescript";
import {
collectTypeScriptFiles,
getPropertyNameText,
resolveRepoRoot,
runAsScript,
toLine,
} from "./lib/ts-guard-utils.mjs";
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
const repoRoot = resolveRepoRoot(import.meta.url);
const acpCoreProtectedSources = [
path.join(repoRoot, "src", "acp"),
@@ -57,50 +63,6 @@ const comparisonOperators = new Set([
const allowedViolations = new Set([]);
function isTestLikeFile(filePath) {
return (
filePath.endsWith(".test.ts") ||
filePath.endsWith(".test-utils.ts") ||
filePath.endsWith(".test-harness.ts") ||
filePath.endsWith(".e2e-harness.ts")
);
}
async function collectTypeScriptFiles(targetPath) {
const stat = await fs.stat(targetPath);
if (stat.isFile()) {
if (!targetPath.endsWith(".ts") || isTestLikeFile(targetPath)) {
return [];
}
return [targetPath];
}
const entries = await fs.readdir(targetPath, { withFileTypes: true });
const files = [];
for (const entry of entries) {
const entryPath = path.join(targetPath, entry.name);
if (entry.isDirectory()) {
files.push(...(await collectTypeScriptFiles(entryPath)));
continue;
}
if (!entry.isFile()) {
continue;
}
if (!entryPath.endsWith(".ts")) {
continue;
}
if (isTestLikeFile(entryPath)) {
continue;
}
files.push(entryPath);
}
return files;
}
function toLine(sourceFile, node) {
return sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile)).line + 1;
}
function isChannelsPropertyAccess(node) {
if (ts.isPropertyAccessExpression(node)) {
return node.name.text === "channels";
@@ -130,13 +92,6 @@ function matchesChannelModuleSpecifier(specifier) {
return channelSegmentRe.test(specifier.replaceAll("\\", "/"));
}
function getPropertyNameText(name) {
if (ts.isIdentifier(name) || ts.isStringLiteral(name) || ts.isNumericLiteral(name)) {
return name.text;
}
return null;
}
const userFacingChannelNameRe =
/\b(?:discord|telegram|slack|signal|imessage|whatsapp|google\s*chat|irc|line|zalo|matrix|msteams|bluebubbles)\b/i;
const systemMarkLiteral = "⚙️";
@@ -348,16 +303,12 @@ export async function main() {
for (const ruleSet of boundaryRuleSets) {
const files = (
await Promise.all(
ruleSet.sources.map(async (sourcePath) => {
try {
return await collectTypeScriptFiles(sourcePath);
} catch (error) {
if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") {
return [];
}
throw error;
}
}),
ruleSet.sources.map(
async (sourcePath) =>
await collectTypeScriptFiles(sourcePath, {
ignoreMissing: true,
}),
),
)
).flat();
for (const filePath of files) {
@@ -389,17 +340,4 @@ export async function main() {
process.exit(1);
}
const isDirectExecution = (() => {
const entry = process.argv[1];
if (!entry) {
return false;
}
return path.resolve(entry) === fileURLToPath(import.meta.url);
})();
if (isDirectExecution) {
main().catch((error) => {
console.error(error);
process.exit(1);
});
}
runAsScript(import.meta.url, main);

View File

@@ -1,11 +1,16 @@
#!/usr/bin/env node
import { promises as fs } from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import ts from "typescript";
import {
collectFileViolations,
getPropertyNameText,
resolveRepoRoot,
runAsScript,
toLine,
} from "./lib/ts-guard-utils.mjs";
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
const repoRoot = resolveRepoRoot(import.meta.url);
const sourceRoots = [path.join(repoRoot, "src"), path.join(repoRoot, "extensions")];
const allowedFiles = new Set([
@@ -31,43 +36,6 @@ const allowedResolverCallNames = new Set([
"resolveIrcEffectiveAllowlists",
]);
function isTestLikeFile(filePath) {
return (
filePath.endsWith(".test.ts") ||
filePath.endsWith(".test-utils.ts") ||
filePath.endsWith(".test-harness.ts") ||
filePath.endsWith(".e2e-harness.ts")
);
}
async function collectTypeScriptFiles(dir) {
const entries = await fs.readdir(dir, { withFileTypes: true });
const out = [];
for (const entry of entries) {
const entryPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
out.push(...(await collectTypeScriptFiles(entryPath)));
continue;
}
if (!entry.isFile() || !entryPath.endsWith(".ts") || isTestLikeFile(entryPath)) {
continue;
}
out.push(entryPath);
}
return out;
}
function toLine(sourceFile, node) {
return sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile)).line + 1;
}
function getPropertyNameText(name) {
if (ts.isIdentifier(name) || ts.isStringLiteral(name) || ts.isNumericLiteral(name)) {
return name.text;
}
return null;
}
function getDeclarationNameText(name) {
if (ts.isIdentifier(name)) {
return name.text;
@@ -190,24 +158,12 @@ function findViolations(content, filePath) {
}
async function main() {
const files = (
await Promise.all(sourceRoots.map(async (root) => await collectTypeScriptFiles(root)))
).flat();
const violations = [];
for (const filePath of files) {
if (allowedFiles.has(filePath)) {
continue;
}
const content = await fs.readFile(filePath, "utf8");
const fileViolations = findViolations(content, filePath);
for (const violation of fileViolations) {
violations.push({
path: path.relative(repoRoot, filePath),
...violation,
});
}
}
const violations = await collectFileViolations({
sourceRoots,
repoRoot,
findViolations,
skipFile: (filePath) => allowedFiles.has(filePath),
});
if (violations.length === 0) {
return;
@@ -223,17 +179,4 @@ async function main() {
process.exit(1);
}
const isDirectExecution = (() => {
const entry = process.argv[1];
if (!entry) {
return false;
}
return path.resolve(entry) === fileURLToPath(import.meta.url);
})();
if (isDirectExecution) {
main().catch((error) => {
console.error(error);
process.exit(1);
});
}
runAsScript(import.meta.url, main);

View File

@@ -2,10 +2,16 @@
import { promises as fs } from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import ts from "typescript";
import {
collectTypeScriptFiles,
resolveRepoRoot,
runAsScript,
toLine,
unwrapExpression,
} from "./lib/ts-guard-utils.mjs";
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
const repoRoot = resolveRepoRoot(import.meta.url);
const sourceRoots = [
path.join(repoRoot, "src", "channels"),
path.join(repoRoot, "src", "infra", "outbound"),
@@ -15,38 +21,6 @@ const sourceRoots = [
];
const allowedCallsites = new Set([path.join(repoRoot, "extensions", "feishu", "src", "dedup.ts")]);
function isTestLikeFile(filePath) {
return (
filePath.endsWith(".test.ts") ||
filePath.endsWith(".test-utils.ts") ||
filePath.endsWith(".test-harness.ts") ||
filePath.endsWith(".e2e-harness.ts")
);
}
async function collectTypeScriptFiles(dir) {
const entries = await fs.readdir(dir, { withFileTypes: true });
const out = [];
for (const entry of entries) {
const entryPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
out.push(...(await collectTypeScriptFiles(entryPath)));
continue;
}
if (!entry.isFile()) {
continue;
}
if (!entryPath.endsWith(".ts")) {
continue;
}
if (isTestLikeFile(entryPath)) {
continue;
}
out.push(entryPath);
}
return out;
}
function collectOsTmpdirImports(sourceFile) {
const osModuleSpecifiers = new Set(["node:os", "os"]);
const osNamespaceOrDefault = new Set();
@@ -81,25 +55,6 @@ function collectOsTmpdirImports(sourceFile) {
return { osNamespaceOrDefault, namedTmpdir };
}
function unwrapExpression(expression) {
let current = expression;
while (true) {
if (ts.isParenthesizedExpression(current)) {
current = current.expression;
continue;
}
if (ts.isAsExpression(current) || ts.isTypeAssertionExpression(current)) {
current = current.expression;
continue;
}
if (ts.isNonNullExpression(current)) {
current = current.expression;
continue;
}
return current;
}
}
export function findMessagingTmpdirCallLines(content, fileName = "source.ts") {
const sourceFile = ts.createSourceFile(fileName, content, ts.ScriptTarget.Latest, true);
const { osNamespaceOrDefault, namedTmpdir } = collectOsTmpdirImports(sourceFile);
@@ -114,11 +69,9 @@ export function findMessagingTmpdirCallLines(content, fileName = "source.ts") {
ts.isIdentifier(callee.expression) &&
osNamespaceOrDefault.has(callee.expression.text)
) {
const line = sourceFile.getLineAndCharacterOfPosition(callee.getStart(sourceFile)).line + 1;
lines.push(line);
lines.push(toLine(sourceFile, callee));
} else if (ts.isIdentifier(callee) && namedTmpdir.has(callee.text)) {
const line = sourceFile.getLineAndCharacterOfPosition(callee.getStart(sourceFile)).line + 1;
lines.push(line);
lines.push(toLine(sourceFile, callee));
}
}
ts.forEachChild(node, visit);
@@ -130,7 +83,14 @@ export function findMessagingTmpdirCallLines(content, fileName = "source.ts") {
export async function main() {
const files = (
await Promise.all(sourceRoots.map(async (dir) => await collectTypeScriptFiles(dir)))
await Promise.all(
sourceRoots.map(
async (dir) =>
await collectTypeScriptFiles(dir, {
ignoreMissing: true,
}),
),
)
).flat();
const violations = [];
@@ -158,17 +118,4 @@ export async function main() {
process.exit(1);
}
const isDirectExecution = (() => {
const entry = process.argv[1];
if (!entry) {
return false;
}
return path.resolve(entry) === fileURLToPath(import.meta.url);
})();
if (isDirectExecution) {
main().catch((error) => {
console.error(error);
process.exit(1);
});
}
runAsScript(import.meta.url, main);

View File

@@ -2,10 +2,16 @@
import { promises as fs } from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import ts from "typescript";
import {
collectTypeScriptFiles,
resolveRepoRoot,
runAsScript,
toLine,
unwrapExpression,
} from "./lib/ts-guard-utils.mjs";
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
const repoRoot = resolveRepoRoot(import.meta.url);
const sourceRoots = [
path.join(repoRoot, "src", "telegram"),
path.join(repoRoot, "src", "discord"),
@@ -65,69 +71,6 @@ const allowedRawFetchCallsites = new Set([
"src/slack/monitor/media.ts:108",
]);
function isTestLikeFile(filePath) {
return (
filePath.endsWith(".test.ts") ||
filePath.endsWith(".test-utils.ts") ||
filePath.endsWith(".test-harness.ts") ||
filePath.endsWith(".e2e-harness.ts") ||
filePath.endsWith(".browser.test.ts") ||
filePath.endsWith(".node.test.ts")
);
}
async function collectTypeScriptFiles(targetPath) {
const stat = await fs.stat(targetPath);
if (stat.isFile()) {
if (!targetPath.endsWith(".ts") || isTestLikeFile(targetPath)) {
return [];
}
return [targetPath];
}
const entries = await fs.readdir(targetPath, { withFileTypes: true });
const files = [];
for (const entry of entries) {
const entryPath = path.join(targetPath, entry.name);
if (entry.isDirectory()) {
if (entry.name === "node_modules") {
continue;
}
files.push(...(await collectTypeScriptFiles(entryPath)));
continue;
}
if (!entry.isFile()) {
continue;
}
if (!entryPath.endsWith(".ts")) {
continue;
}
if (isTestLikeFile(entryPath)) {
continue;
}
files.push(entryPath);
}
return files;
}
function unwrapExpression(expression) {
let current = expression;
while (true) {
if (ts.isParenthesizedExpression(current)) {
current = current.expression;
continue;
}
if (ts.isAsExpression(current) || ts.isTypeAssertionExpression(current)) {
current = current.expression;
continue;
}
if (ts.isNonNullExpression(current)) {
current = current.expression;
continue;
}
return current;
}
}
function isRawFetchCall(expression) {
const callee = unwrapExpression(expression);
if (ts.isIdentifier(callee)) {
@@ -148,9 +91,7 @@ export function findRawFetchCallLines(content, fileName = "source.ts") {
const lines = [];
const visit = (node) => {
if (ts.isCallExpression(node) && isRawFetchCall(node.expression)) {
const line =
sourceFile.getLineAndCharacterOfPosition(node.expression.getStart(sourceFile)).line + 1;
lines.push(line);
lines.push(toLine(sourceFile, node.expression));
}
ts.forEachChild(node, visit);
};
@@ -161,13 +102,13 @@ export function findRawFetchCallLines(content, fileName = "source.ts") {
export async function main() {
const files = (
await Promise.all(
sourceRoots.map(async (sourceRoot) => {
try {
return await collectTypeScriptFiles(sourceRoot);
} catch {
return [];
}
}),
sourceRoots.map(
async (sourceRoot) =>
await collectTypeScriptFiles(sourceRoot, {
extraTestSuffixes: [".browser.test.ts", ".node.test.ts"],
ignoreMissing: true,
}),
),
)
).flat();
@@ -198,17 +139,4 @@ export async function main() {
process.exit(1);
}
const isDirectExecution = (() => {
const entry = process.argv[1];
if (!entry) {
return false;
}
return path.resolve(entry) === fileURLToPath(import.meta.url);
})();
if (isDirectExecution) {
main().catch((error) => {
console.error(error);
process.exit(1);
});
}
runAsScript(import.meta.url, main);

View File

@@ -2,63 +2,19 @@
import { promises as fs } from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import ts from "typescript";
import {
collectTypeScriptFiles,
resolveRepoRoot,
runAsScript,
toLine,
unwrapExpression,
} from "./lib/ts-guard-utils.mjs";
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
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 isTestFile(filePath) {
return (
filePath.endsWith(".test.ts") ||
filePath.endsWith(".browser.test.ts") ||
filePath.endsWith(".node.test.ts")
);
}
async function collectTypeScriptFiles(dir) {
const entries = await fs.readdir(dir, { withFileTypes: true });
const out = [];
for (const entry of entries) {
const entryPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
out.push(...(await collectTypeScriptFiles(entryPath)));
continue;
}
if (!entry.isFile()) {
continue;
}
if (!entryPath.endsWith(".ts")) {
continue;
}
if (isTestFile(entryPath)) {
continue;
}
out.push(entryPath);
}
return out;
}
function unwrapExpression(expression) {
let current = expression;
while (true) {
if (ts.isParenthesizedExpression(current)) {
current = current.expression;
continue;
}
if (ts.isAsExpression(current) || ts.isTypeAssertionExpression(current)) {
current = current.expression;
continue;
}
if (ts.isNonNullExpression(current)) {
current = current.expression;
continue;
}
return current;
}
}
function asPropertyAccess(expression) {
if (ts.isPropertyAccessExpression(expression)) {
return expression;
@@ -87,9 +43,7 @@ export function findRawWindowOpenLines(content, fileName = "source.ts") {
const visit = (node) => {
if (ts.isCallExpression(node) && isRawWindowOpenCall(node.expression)) {
const line =
sourceFile.getLineAndCharacterOfPosition(node.expression.getStart(sourceFile)).line + 1;
lines.push(line);
lines.push(toLine(sourceFile, node.expression));
}
ts.forEachChild(node, visit);
};
@@ -99,7 +53,10 @@ export function findRawWindowOpenLines(content, fileName = "source.ts") {
}
export async function main() {
const files = await collectTypeScriptFiles(uiSourceDir);
const files = await collectTypeScriptFiles(uiSourceDir, {
extraTestSuffixes: [".browser.test.ts", ".node.test.ts"],
ignoreMissing: true,
});
const violations = [];
for (const filePath of files) {
@@ -126,17 +83,4 @@ export async function main() {
process.exit(1);
}
const isDirectExecution = (() => {
const entry = process.argv[1];
if (!entry) {
return false;
}
return path.resolve(entry) === fileURLToPath(import.meta.url);
})();
if (isDirectExecution) {
main().catch((error) => {
console.error(error);
process.exit(1);
});
}
runAsScript(import.meta.url, main);

View File

@@ -1,50 +1,18 @@
#!/usr/bin/env node
import { promises as fs } from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import ts from "typescript";
import {
collectFileViolations,
getPropertyNameText,
resolveRepoRoot,
runAsScript,
toLine,
} from "./lib/ts-guard-utils.mjs";
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
const repoRoot = resolveRepoRoot(import.meta.url);
const sourceRoots = [path.join(repoRoot, "src"), path.join(repoRoot, "extensions")];
function isTestLikeFile(filePath) {
return (
filePath.endsWith(".test.ts") ||
filePath.endsWith(".test-utils.ts") ||
filePath.endsWith(".test-harness.ts") ||
filePath.endsWith(".e2e-harness.ts")
);
}
async function collectTypeScriptFiles(dir) {
const entries = await fs.readdir(dir, { withFileTypes: true });
const out = [];
for (const entry of entries) {
const entryPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
out.push(...(await collectTypeScriptFiles(entryPath)));
continue;
}
if (!entry.isFile() || !entryPath.endsWith(".ts") || isTestLikeFile(entryPath)) {
continue;
}
out.push(entryPath);
}
return out;
}
function toLine(sourceFile, node) {
return sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile)).line + 1;
}
function getPropertyNameText(name) {
if (ts.isIdentifier(name) || ts.isStringLiteral(name) || ts.isNumericLiteral(name)) {
return name.text;
}
return null;
}
function isUndefinedLikeExpression(node) {
if (ts.isIdentifier(node) && node.text === "undefined") {
return true;
@@ -114,21 +82,11 @@ function findViolations(content, filePath) {
}
async function main() {
const files = (
await Promise.all(sourceRoots.map(async (root) => await collectTypeScriptFiles(root)))
).flat();
const violations = [];
for (const filePath of files) {
const content = await fs.readFile(filePath, "utf8");
const fileViolations = findViolations(content, filePath);
for (const violation of fileViolations) {
violations.push({
path: path.relative(repoRoot, filePath),
...violation,
});
}
}
const violations = await collectFileViolations({
sourceRoots,
repoRoot,
findViolations,
});
if (violations.length === 0) {
return;
@@ -141,17 +99,4 @@ async function main() {
process.exit(1);
}
const isDirectExecution = (() => {
const entry = process.argv[1];
if (!entry) {
return false;
}
return path.resolve(entry) === fileURLToPath(import.meta.url);
})();
if (isDirectExecution) {
main().catch((error) => {
console.error(error);
process.exit(1);
});
}
runAsScript(import.meta.url, main);

View File

@@ -340,39 +340,17 @@ async function discordApi<T>(params: {
body?: unknown;
retries?: number;
}): Promise<T> {
const retries = params.retries ?? 6;
for (let attempt = 0; attempt <= retries; attempt += 1) {
const response = await fetch(`${DISCORD_API_BASE}${params.path}`, {
method: params.method,
headers: {
Authorization: params.authHeader,
"Content-Type": "application/json",
},
body: params.body === undefined ? undefined : JSON.stringify(params.body),
});
if (response.status === 429) {
const body = (await response.json().catch(() => ({}))) as { retry_after?: number };
const waitSeconds = typeof body.retry_after === "number" ? body.retry_after : 1;
await sleep(Math.ceil(waitSeconds * 1000));
continue;
}
if (!response.ok) {
const text = await response.text().catch(() => "");
throw new Error(
`Discord API ${params.method} ${params.path} failed: ${response.status} ${response.statusText}${text ? ` :: ${text}` : ""}`,
);
}
if (response.status === 204) {
return undefined as T;
}
return (await response.json()) as T;
}
throw new Error(`Discord API ${params.method} ${params.path} exceeded retry budget.`);
return requestDiscordJson<T>({
method: params.method,
path: params.path,
headers: {
Authorization: params.authHeader,
"Content-Type": "application/json",
},
body: params.body,
retries: params.retries,
errorPrefix: "Discord API",
});
}
async function discordWebhookApi<T>(params: {
@@ -383,15 +361,33 @@ async function discordWebhookApi<T>(params: {
query?: string;
retries?: number;
}): Promise<T> {
const retries = params.retries ?? 6;
const suffix = params.query ? `?${params.query}` : "";
const path = `/webhooks/${encodeURIComponent(params.webhookId)}/${encodeURIComponent(params.webhookToken)}${suffix}`;
return requestDiscordJson<T>({
method: params.method,
path,
headers: {
"Content-Type": "application/json",
},
body: params.body,
retries: params.retries,
errorPrefix: "Discord webhook API",
});
}
async function requestDiscordJson<T>(params: {
method: string;
path: string;
headers: Record<string, string>;
body?: unknown;
retries?: number;
errorPrefix: string;
}): Promise<T> {
const retries = params.retries ?? 6;
for (let attempt = 0; attempt <= retries; attempt += 1) {
const response = await fetch(`${DISCORD_API_BASE}${path}`, {
const response = await fetch(`${DISCORD_API_BASE}${params.path}`, {
method: params.method,
headers: {
"Content-Type": "application/json",
},
headers: params.headers,
body: params.body === undefined ? undefined : JSON.stringify(params.body),
});
@@ -405,7 +401,7 @@ async function discordWebhookApi<T>(params: {
if (!response.ok) {
const text = await response.text().catch(() => "");
throw new Error(
`Discord webhook API ${params.method} ${path} failed: ${response.status} ${response.statusText}${text ? ` :: ${text}` : ""}`,
`${params.errorPrefix} ${params.method} ${params.path} failed: ${response.status} ${response.statusText}${text ? ` :: ${text}` : ""}`,
);
}
@@ -416,7 +412,7 @@ async function discordWebhookApi<T>(params: {
return (await response.json()) as T;
}
throw new Error(`Discord webhook API ${params.method} ${path} exceeded retry budget.`);
throw new Error(`${params.errorPrefix} ${params.method} ${params.path} exceeded retry budget.`);
}
async function readThreadBindings(filePath: string): Promise<ThreadBindingRecord[]> {
@@ -487,6 +483,24 @@ function toRecentMessageRow(message: DiscordMessage) {
};
}
async function loadParentRecentMessages(params: {
args: Args;
readAuthHeader: string;
}): Promise<DiscordMessage[]> {
if (params.args.driverMode === "openclaw") {
return await readMessagesWithOpenclaw({
openclawBin: params.args.openclawBin,
target: params.args.channelId,
limit: 20,
});
}
return await discordApi<DiscordMessage[]>({
method: "GET",
path: `/channels/${encodeURIComponent(params.args.channelId)}/messages?limit=20`,
authHeader: params.readAuthHeader,
});
}
function printOutput(params: { json: boolean; payload: SuccessResult | FailureResult }) {
if (params.json) {
// eslint-disable-next-line no-console
@@ -714,18 +728,7 @@ async function run(): Promise<SuccessResult | FailureResult> {
if (!winningBinding?.threadId || !winningBinding?.targetSessionKey) {
let parentRecent: DiscordMessage[] = [];
try {
parentRecent =
args.driverMode === "openclaw"
? await readMessagesWithOpenclaw({
openclawBin: args.openclawBin,
target: args.channelId,
limit: 20,
})
: await discordApi<DiscordMessage[]>({
method: "GET",
path: `/channels/${encodeURIComponent(args.channelId)}/messages?limit=20`,
authHeader: readAuthHeader,
});
parentRecent = await loadParentRecentMessages({ args, readAuthHeader });
} catch {
// Best effort diagnostics only.
}
@@ -782,18 +785,7 @@ async function run(): Promise<SuccessResult | FailureResult> {
if (!ackMessage) {
let parentRecent: DiscordMessage[] = [];
try {
parentRecent =
args.driverMode === "openclaw"
? await readMessagesWithOpenclaw({
openclawBin: args.openclawBin,
target: args.channelId,
limit: 20,
})
: await discordApi<DiscordMessage[]>({
method: "GET",
path: `/channels/${encodeURIComponent(args.channelId)}/messages?limit=20`,
authHeader: readAuthHeader,
});
parentRecent = await loadParentRecentMessages({ args, readAuthHeader });
} catch {
// Best effort diagnostics only.
}

View File

@@ -0,0 +1,147 @@
import { promises as fs } from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import ts from "typescript";
const baseTestSuffixes = [".test.ts", ".test-utils.ts", ".test-harness.ts", ".e2e-harness.ts"];
export function resolveRepoRoot(importMetaUrl) {
return path.resolve(path.dirname(fileURLToPath(importMetaUrl)), "..", "..");
}
export function isTestLikeTypeScriptFile(filePath, options = {}) {
const extraTestSuffixes = options.extraTestSuffixes ?? [];
return [...baseTestSuffixes, ...extraTestSuffixes].some((suffix) => filePath.endsWith(suffix));
}
export async function collectTypeScriptFiles(targetPath, options = {}) {
const includeTests = options.includeTests ?? false;
const extraTestSuffixes = options.extraTestSuffixes ?? [];
const skipNodeModules = options.skipNodeModules ?? true;
const ignoreMissing = options.ignoreMissing ?? false;
let stat;
try {
stat = await fs.stat(targetPath);
} catch (error) {
if (
ignoreMissing &&
error &&
typeof error === "object" &&
"code" in error &&
error.code === "ENOENT"
) {
return [];
}
throw error;
}
if (stat.isFile()) {
if (!targetPath.endsWith(".ts")) {
return [];
}
if (!includeTests && isTestLikeTypeScriptFile(targetPath, { extraTestSuffixes })) {
return [];
}
return [targetPath];
}
const entries = await fs.readdir(targetPath, { withFileTypes: true });
const out = [];
for (const entry of entries) {
const entryPath = path.join(targetPath, entry.name);
if (entry.isDirectory()) {
if (skipNodeModules && entry.name === "node_modules") {
continue;
}
out.push(...(await collectTypeScriptFiles(entryPath, options)));
continue;
}
if (!entry.isFile() || !entryPath.endsWith(".ts")) {
continue;
}
if (!includeTests && isTestLikeTypeScriptFile(entryPath, { extraTestSuffixes })) {
continue;
}
out.push(entryPath);
}
return out;
}
export async function collectFileViolations(params) {
const files = (
await Promise.all(
params.sourceRoots.map(
async (root) =>
await collectTypeScriptFiles(root, {
ignoreMissing: true,
extraTestSuffixes: params.extraTestSuffixes,
}),
),
)
).flat();
const violations = [];
for (const filePath of files) {
if (params.skipFile?.(filePath)) {
continue;
}
const content = await fs.readFile(filePath, "utf8");
const fileViolations = params.findViolations(content, filePath);
for (const violation of fileViolations) {
violations.push({
path: path.relative(params.repoRoot, filePath),
...violation,
});
}
}
return violations;
}
export function toLine(sourceFile, node) {
return sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile)).line + 1;
}
export function getPropertyNameText(name) {
if (ts.isIdentifier(name) || ts.isStringLiteral(name) || ts.isNumericLiteral(name)) {
return name.text;
}
return null;
}
export function unwrapExpression(expression) {
let current = expression;
while (true) {
if (ts.isParenthesizedExpression(current)) {
current = current.expression;
continue;
}
if (ts.isAsExpression(current) || ts.isTypeAssertionExpression(current)) {
current = current.expression;
continue;
}
if (ts.isNonNullExpression(current)) {
current = current.expression;
continue;
}
return current;
}
}
export function isDirectExecution(importMetaUrl) {
const entry = process.argv[1];
if (!entry) {
return false;
}
return path.resolve(entry) === fileURLToPath(importMetaUrl);
}
export function runAsScript(importMetaUrl, main) {
if (!isDirectExecution(importMetaUrl)) {
return;
}
main().catch((error) => {
console.error(error);
process.exit(1);
});
}

View File

@@ -6,12 +6,45 @@ import path from "node:path";
import { describe, expect, it } from "vitest";
const SCRIPT = path.join(process.cwd(), "scripts", "ios-team-id.sh");
const XCODE_PLIST_PATH = path.join("Library", "Preferences", "com.apple.dt.Xcode.plist");
const DEFAULTS_WITH_ACCOUNT_SCRIPT = `#!/usr/bin/env bash
if [[ "$3" == "DVTDeveloperAccountManagerAppleIDLists" ]]; then
echo '(identifier = "dev@example.com";)'
exit 0
fi
exit 0`;
async function writeExecutable(filePath: string, body: string): Promise<void> {
await writeFile(filePath, body, "utf8");
chmodSync(filePath, 0o755);
}
async function setupFixture(params?: {
provisioningProfiles?: Record<string, string>;
}): Promise<{ homeDir: string; binDir: string }> {
const homeDir = await mkdtemp(path.join(os.tmpdir(), "openclaw-ios-team-id-"));
const binDir = path.join(homeDir, "bin");
await mkdir(binDir, { recursive: true });
await mkdir(path.join(homeDir, "Library", "Preferences"), { recursive: true });
await writeFile(path.join(homeDir, XCODE_PLIST_PATH), "");
const provisioningProfiles = params?.provisioningProfiles;
if (provisioningProfiles) {
const profilesDir = path.join(homeDir, "Library", "MobileDevice", "Provisioning Profiles");
await mkdir(profilesDir, { recursive: true });
for (const [name, body] of Object.entries(provisioningProfiles)) {
await writeFile(path.join(profilesDir, name), body);
}
}
return { homeDir, binDir };
}
async function writeDefaultsWithSignedInAccount(binDir: string): Promise<void> {
await writeExecutable(path.join(binDir, "defaults"), DEFAULTS_WITH_ACCOUNT_SCRIPT);
}
function runScript(
homeDir: string,
extraEnv: Record<string, string> = {},
@@ -47,33 +80,18 @@ function runScript(
describe("scripts/ios-team-id.sh", () => {
it("falls back to Xcode-managed provisioning profiles when preference teams are empty", async () => {
const homeDir = await mkdtemp(path.join(os.tmpdir(), "openclaw-ios-team-id-"));
const binDir = path.join(homeDir, "bin");
await mkdir(binDir, { recursive: true });
await mkdir(path.join(homeDir, "Library", "Preferences"), { recursive: true });
await mkdir(path.join(homeDir, "Library", "MobileDevice", "Provisioning Profiles"), {
recursive: true,
const { homeDir, binDir } = await setupFixture({
provisioningProfiles: {
"one.mobileprovision": "stub",
},
});
await writeFile(path.join(homeDir, "Library", "Preferences", "com.apple.dt.Xcode.plist"), "");
await writeFile(
path.join(homeDir, "Library", "MobileDevice", "Provisioning Profiles", "one.mobileprovision"),
"stub",
);
await writeExecutable(
path.join(binDir, "plutil"),
`#!/usr/bin/env bash
echo '{}'`,
);
await writeExecutable(
path.join(binDir, "defaults"),
`#!/usr/bin/env bash
if [[ "$3" == "DVTDeveloperAccountManagerAppleIDLists" ]]; then
echo '(identifier = "dev@example.com";)'
exit 0
fi
exit 0`,
);
await writeDefaultsWithSignedInAccount(binDir);
await writeExecutable(
path.join(binDir, "security"),
`#!/usr/bin/env bash
@@ -101,11 +119,7 @@ exit 0`,
});
it("prints actionable guidance when Xcode account exists but no Team ID is resolvable", async () => {
const homeDir = await mkdtemp(path.join(os.tmpdir(), "openclaw-ios-team-id-"));
const binDir = path.join(homeDir, "bin");
await mkdir(binDir, { recursive: true });
await mkdir(path.join(homeDir, "Library", "Preferences"), { recursive: true });
await writeFile(path.join(homeDir, "Library", "Preferences", "com.apple.dt.Xcode.plist"), "");
const { homeDir, binDir } = await setupFixture();
await writeExecutable(
path.join(binDir, "plutil"),
@@ -135,37 +149,19 @@ exit 1`,
});
it("honors IOS_PREFERRED_TEAM_ID when multiple profile teams are available", async () => {
const homeDir = await mkdtemp(path.join(os.tmpdir(), "openclaw-ios-team-id-"));
const binDir = path.join(homeDir, "bin");
await mkdir(binDir, { recursive: true });
await mkdir(path.join(homeDir, "Library", "Preferences"), { recursive: true });
await mkdir(path.join(homeDir, "Library", "MobileDevice", "Provisioning Profiles"), {
recursive: true,
const { homeDir, binDir } = await setupFixture({
provisioningProfiles: {
"one.mobileprovision": "stub1",
"two.mobileprovision": "stub2",
},
});
await writeFile(path.join(homeDir, "Library", "Preferences", "com.apple.dt.Xcode.plist"), "");
await writeFile(
path.join(homeDir, "Library", "MobileDevice", "Provisioning Profiles", "one.mobileprovision"),
"stub1",
);
await writeFile(
path.join(homeDir, "Library", "MobileDevice", "Provisioning Profiles", "two.mobileprovision"),
"stub2",
);
await writeExecutable(
path.join(binDir, "plutil"),
`#!/usr/bin/env bash
echo '{}'`,
);
await writeExecutable(
path.join(binDir, "defaults"),
`#!/usr/bin/env bash
if [[ "$3" == "DVTDeveloperAccountManagerAppleIDLists" ]]; then
echo '(identifier = "dev@example.com";)'
exit 0
fi
exit 0`,
);
await writeDefaultsWithSignedInAccount(binDir);
await writeExecutable(
path.join(binDir, "security"),
`#!/usr/bin/env bash
@@ -194,26 +190,14 @@ exit 0`,
});
it("matches preferred team IDs even when parser output uses CRLF line endings", async () => {
const homeDir = await mkdtemp(path.join(os.tmpdir(), "openclaw-ios-team-id-"));
const binDir = path.join(homeDir, "bin");
await mkdir(binDir, { recursive: true });
await mkdir(path.join(homeDir, "Library", "Preferences"), { recursive: true });
await writeFile(path.join(homeDir, "Library", "Preferences", "com.apple.dt.Xcode.plist"), "");
const { homeDir, binDir } = await setupFixture();
await writeExecutable(
path.join(binDir, "plutil"),
`#!/usr/bin/env bash
echo '{}'`,
);
await writeExecutable(
path.join(binDir, "defaults"),
`#!/usr/bin/env bash
if [[ "$3" == "DVTDeveloperAccountManagerAppleIDLists" ]]; then
echo '(identifier = "dev@example.com";)'
exit 0
fi
exit 0`,
);
await writeDefaultsWithSignedInAccount(binDir);
await writeExecutable(
path.join(binDir, "fake-python"),
`#!/usr/bin/env bash