Replace NodeJS dict aliases with maps and named field reads #24

Merged
temeddix merged 20 commits from honest-dict-types into main 2026-10-20 05:01:51 +00:00
14 changed files with 147 additions and 99 deletions
Showing only changes of commit d879b04e46 - Show all commits
+13 -12
View File
@@ -195,7 +195,7 @@ const rules: DummyRuleMap = {
complexity: ["error", 26],
"max-depth": ["error", 6],
"max-lines": ["error", { max: 2048, skipBlankLines: false, skipComments: false }],
"max-lines-per-function": ["error", 96],
"max-lines-per-function": ["error", 98],
"max-params": ["error", 4],
"max-statements": ["error", 32],
"import/max-dependencies": ["error", { max: 13 }],
@@ -242,11 +242,6 @@ const rules: DummyRuleMap = {
"import/no-named-export": "off",
// Collides with `typescript/consistent-type-imports`.
"import/consistent-type-specifier-style": "off",
// Collides with `require-await`, which fails an `async` wrapper with no `await`.
"typescript/promise-function-async": "off",
// Collides with explicit-types `no-undefined-type`: a `let` that `try`
// assigns would need `T | undefined`.
"init-declarations": "off",
// Collides with literal Gitea Actions `${{ }}` expressions in workflow strings.
"no-template-curly-in-string": "off",
// Node code, everywhere: this action runs on Node.
@@ -256,18 +251,18 @@ const rules: DummyRuleMap = {
"unicorn/no-process-exit": "off",
// The action reports to the job log through the console.
"no-console": "off",
// PENDING USER: union members are values in profiles.json, the Gitea API,
// and posted comments; renaming them changes behavior.
"explicit-types/upper-snake-string-unions": "off",
// PENDING USER: a fitting `name` needs a constructor and changes logged
// error names.
"unicorn/custom-error-definition": "off",
// Sequential awaits are intentional; workarounds cost more than they save.
"no-await-in-loop": "off",
// Not type-aware; TypeScript's `noImplicitReturns` covers it.
"consistent-return": "off",
// TypeScript's type checking covers exhaustive switches.
"typescript/switch-exhaustiveness-check": "off",
// PENDING USER: union members are values in profiles.json, the Gitea API,
// and posted comments; renaming them changes behavior.
"explicit-types/upper-snake-string-unions": "off",
// PENDING USER: a fitting `name` needs a constructor and changes logged
// error names.
"unicorn/custom-error-definition": "off",
// Off, too much churn:
// Literal numbers read better than a constant per use.
"no-magic-numbers": "off",
@@ -317,6 +312,12 @@ export const projectOverrides: OxlintOverride[] = [
files: ["vite.config.ts", "scripts/**"],
rules: nodeRules,
},
// exitCode must return the promise it marks handled; an async wrapper's
// promise would reject unhandled before callers await it.
{
files: ["src/processes/child.ts"],
rules: { "typescript/promise-function-async": "off" },
},
];
/**
+21 -18
View File
@@ -16,14 +16,16 @@ test("retries once after an invalid refresh token", async () => {
let attempts = 0;
let relogins = 0;
const result = await retryInvalidToken(
() =>
Promise.resolve({
async () => {
const resolved = await Promise.resolve({
code: 1,
error: (attempts += 1) === 1 ? "ERROR: invalid_refresh_token" : "still failed",
}),
() => {
});
return resolved;
},
async () => {
relogins += 1;
return Promise.resolve();
await Promise.resolve();
},
);
@@ -38,16 +40,17 @@ test("does not retry an unrelated failure", async () => {
let attempts = 0;
let relogins = 0;
await retryInvalidToken(
() => {
async () => {
attempts += 1;
return Promise.resolve({
const resolved = await Promise.resolve({
code: 1,
error: "rate limited",
});
return resolved;
},
() => {
async () => {
relogins += 1;
return Promise.resolve();
await Promise.resolve();
},
);
@@ -85,14 +88,16 @@ test("re-logs in after the job 9837 revoked-session stderr", async () => {
let relogins = 0;
let attempts = 0;
const result = await retryInvalidToken(
() =>
Promise.resolve({
async () => {
const resolved = await Promise.resolve({
code: (attempts += 1) > 1 ? 0 : 1,
error: attempts === 1 ? REVOKED_CODEX.join("\n") : "",
}),
() => {
});
return resolved;
},
async () => {
relogins += 1;
return Promise.resolve();
await Promise.resolve();
},
);
@@ -171,10 +176,8 @@ test.skipIf(IS_WINDOWS)("rejects a symlink or malformed Codex login", async () =
await writeFile(malformed, '{"tokens":{}}', { mode: 0o600 });
await symlink(canonical, linked);
await expect(() => persistCodexAuth(linked, canonical)).rejects.toThrow("regular file");
await expect(() => persistCodexAuth(malformed, canonical)).rejects.toThrow(
"invalid Codex login",
);
await expect(persistCodexAuth(linked, canonical)).rejects.toThrow("regular file");
await expect(persistCodexAuth(malformed, canonical)).rejects.toThrow("invalid Codex login");
} finally {
await rm(root, { recursive: true });
}
+1 -1
View File
@@ -83,7 +83,7 @@ async function readCodexAuth(path: string): Promise<string> {
throw new Error("Codex login must be private");
}
const text = await readFile(path, "utf8");
let value: unknown;
let value: unknown = null;
try {
value = JSON.parse(text);
} catch {
+1 -1
View File
@@ -52,7 +52,7 @@ export function openCodeError(output: string): Error | null {
if (line.trim() === "") {
continue;
}
let event: unknown;
let event: unknown = null;
try {
event = JSON.parse(line);
} catch {
+34 -26
View File
@@ -126,9 +126,10 @@ function requestBody(init?: RequestInit): unknown {
// A fake Gitea that records each request body and answers with `status`.
function recordingFetch(bodies: unknown[], status = 201): Fetcher {
return (_url, init) => {
return async (_url, init) => {
bodies.push(requestBody(init));
return Promise.resolve(new Response(null, { status }));
const resolved = await Promise.resolve(new Response(null, { status }));
return resolved;
};
}
@@ -136,9 +137,10 @@ test("posts a successful issue result and removes runtime credentials", async ()
const result = await fixture({ ok: true, body: "done" });
const requests: RecordedRequest[] = [];
await postRuntime(result.manifestPath, context, (url, init) => {
await postRuntime(result.manifestPath, context, async (url, init) => {
requests.push({ url, init });
return Promise.resolve(new Response(null, { status: 201 }));
const resolved = await Promise.resolve(new Response(null, { status: 201 }));
return resolved;
});
expect(requests.length).toBe(1);
@@ -166,9 +168,12 @@ test("retries a rejected inline review without anchors", async () => {
...context,
eventName: "pull_request",
},
(_url, init) => {
async (_url, init) => {
bodies.push(requestBody(init));
return Promise.resolve(new Response(null, { status: bodies.length === 1 ? 422 : 200 }));
const resolved = await Promise.resolve(
new Response(null, { status: bodies.length === 1 ? 422 : 200 }),
);
return resolved;
},
);
@@ -193,9 +198,9 @@ test("reports each tried profile and keeps the action failed", async () => {
});
const bodies: unknown[] = [];
await expect(() =>
postRuntime(result.manifestPath, context, recordingFetch(bodies)),
).rejects.toThrow("agent phase failed");
await expect(postRuntime(result.manifestPath, context, recordingFetch(bodies))).rejects.toThrow(
"agent phase failed",
);
expect(bodies).toStrictEqual([
{
@@ -224,9 +229,9 @@ test("drops failure entries that are not known labels", async () => {
);
const bodies: unknown[] = [];
await expect(() =>
postRuntime(result.manifestPath, context, recordingFetch(bodies)),
).rejects.toThrow("agent phase failed");
await expect(postRuntime(result.manifestPath, context, recordingFetch(bodies))).rejects.toThrow(
"agent phase failed",
);
expect(bodies).toStrictEqual([
{
@@ -240,9 +245,9 @@ test("reports a generic failure when the runner wrote no outcome", async () => {
await rm(result.outcomeFile);
const bodies: unknown[] = [];
await expect(() =>
postRuntime(result.manifestPath, context, recordingFetch(bodies)),
).rejects.toThrow("agent phase failed");
await expect(postRuntime(result.manifestPath, context, recordingFetch(bodies))).rejects.toThrow(
"agent phase failed",
);
expect(bodies).toStrictEqual([{ body: "AI bot failed.\n\n> Profile: none" }]);
assert(await missing(result.runtimeDir));
@@ -251,10 +256,11 @@ test("reports a generic failure when the runner wrote no outcome", async () => {
test("removes runtime credentials when posting fails", async () => {
const result = await fixture({ ok: true, body: "done" });
await expect(() =>
postRuntime(result.manifestPath, context, () =>
Promise.resolve(new Response("failed", { status: 500 })),
),
await expect(
postRuntime(result.manifestPath, context, async () => {
const resolved = await Promise.resolve(new Response("failed", { status: 500 }));
return resolved;
}),
).rejects.toThrow("500");
assert(await missing(result.runtimeDir));
});
@@ -263,10 +269,11 @@ test("preserves a Gitea transport failure without retrying a comment", async ()
const result = await fixture({ ok: true, body: "done" });
let requests = 0;
await expect(() =>
postRuntime(result.manifestPath, context, () => {
await expect(
postRuntime(result.manifestPath, context, async () => {
requests += 1;
return Promise.reject(new Error(`network failure ${requests}`));
await Promise.resolve();
throw new Error(`network failure ${requests}`);
}),
).rejects.toThrow("network failure 1");
@@ -279,10 +286,11 @@ test.skipIf(IS_WINDOWS)("refuses to follow an agent artifact symlink", async ()
await symlink(result.reviewerFile, result.reviewPath);
let requests = 0;
await expect(() =>
postRuntime(result.manifestPath, context, () => {
await expect(
postRuntime(result.manifestPath, context, async () => {
requests += 1;
return Promise.resolve(new Response(null, { status: 200 }));
const resolved = await Promise.resolve(new Response(null, { status: 200 }));
return resolved;
}),
).rejects.toThrow("regular private file");
@@ -294,7 +302,7 @@ test("reports an attributed failure when a pull request review is missing", asyn
const result = await fixture({ ok: true, body: "done" });
const bodies: unknown[] = [];
await expect(() =>
await expect(
postRuntime(
result.manifestPath,
{
+9 -7
View File
@@ -121,7 +121,7 @@ async function readAgentArtifact(
if (profile === undefined) {
throw new Error(`${name}: outside profile home`);
}
let stat: Stats;
let stat: Stats | null = null;
try {
stat = await lstat(path);
} catch (error) {
@@ -152,7 +152,7 @@ export async function postRuntime(
const reviewerText = await readFile(manifest.reviewerFile, "utf8");
const reviewerToken = reviewerText.trim();
const request = async (path: string, body: unknown): Promise<void> => {
let response: Response;
let response: Response | null = null;
try {
response = await fetcher(`${context.apiUrl}/${path}`, {
method: "POST",
@@ -170,12 +170,13 @@ export async function postRuntime(
throw new GiteaRequestError(`${path}: ${response.status} ${await response.text()}`);
}
};
const postComment = (body: string): Promise<void> =>
request(`repos/${context.repository}/issues/${context.issueIndex}/comments`, {
const postComment = async (body: string): Promise<void> => {
await request(`repos/${context.repository}/issues/${context.issueIndex}/comments`, {
body: stripAnsi(body),
});
};
let outcome: RuntimeOutcome;
let outcome: RuntimeOutcome | null = null;
try {
outcome = await readOutcome(manifest.outcomeFile);
} catch (error) {
@@ -201,8 +202,8 @@ export async function postRuntime(
const [event, mark] = VERDICTS.get(verdict.trim()) ?? ["COMMENT", "💬"];
const anchorText = await readAgentArtifact(manifest, outcome.anchorsPath, "anchors.json");
const anchors = anchorText === null ? [] : parseAnchors(anchorText, outcome.anchorsPath);
const postReview = (entries: Anchor[]): Promise<void> =>
request(`repos/${context.repository}/pulls/${context.issueIndex}/reviews`, {
const postReview = async (entries: Anchor[]): Promise<void> => {
await request(`repos/${context.repository}/pulls/${context.issueIndex}/reviews`, {
body: stripAnsi(
withProfile([`${mark} ${verdict.trim()}`, ...rest].join("\n"), outcome.profile),
),
@@ -214,6 +215,7 @@ export async function postRuntime(
old_position: side === "old" ? line : 0,
})),
});
};
await postReview(anchors).catch(async (error: unknown) => {
if (anchors.length === 0) {
throw error;
+1 -1
View File
@@ -49,7 +49,7 @@ test.skipIf(IS_WINDOWS)("refuses to read a profile result symlink as root", asyn
await writeFile(secret, "reviewer-secret", { mode: 0o600 });
await symlink(secret, result);
await expect(() => readProfileFile(profile(currentUid(), root), result)).rejects.toThrow(
await expect(readProfileFile(profile(currentUid(), root), result)).rejects.toThrow(
"regular private file",
);
} finally {
+4 -1
View File
@@ -17,7 +17,10 @@ async function profileProcessIds(uid: number): Promise<number[]> {
.filter((entry) => entry.isDirectory() && /^\d+$/u.test(entry.name))
.map((entry) => entry.name);
const statuses = await Promise.all(
pids.map((pid) => readFile(`/proc/${pid}/status`, "utf8").catch(() => null)),
pids.map(async (pid) => {
const status = await readFile(`/proc/${pid}/status`, "utf8").catch(() => null);
return status;
}),
);
return pids
.filter((_pid, index) => {
+20 -15
View File
@@ -23,12 +23,13 @@ test("falls through only after usage exhaustion", async () => {
{ type: "claude", model: "claude-sonnet-5", token: "first" },
{ type: "codex", model: "gpt-5.6-terra" },
],
(profile) => {
async (profile) => {
visited.push(profile.type);
if (profile.type === "claude") {
throw new Error("You've hit your usage limit");
}
return Promise.resolve("done");
const resolved = await Promise.resolve("done");
return resolved;
},
);
@@ -42,11 +43,12 @@ test("recognizes Claude's weekly limit message", async () => {
{ type: "claude", model: "claude-sonnet-5", token: "first" },
{ type: "codex", model: "gpt-5.6-terra" },
],
(profile) => {
async (profile) => {
if (profile.type === "claude") {
throw new Error("You've hit your weekly limit · resets 12am (UTC)");
}
return Promise.resolve("fallback");
const resolved = await Promise.resolve("fallback");
return resolved;
},
);
@@ -55,20 +57,22 @@ test("recognizes Claude's weekly limit message", async () => {
test("recognizes provider API exhaustion status", async () => {
const results = await Promise.all(
["API Error: 402 Insufficient funds", "HTTP error: 429"].map((message) =>
runProfiles(
["API Error: 402 Insufficient funds", "HTTP error: 429"].map(async (message) => {
const result = await runProfiles(
[
{ type: "claude", model: "claude-sonnet-5", token: "first" },
{ type: "codex", model: "gpt-5.6-terra" },
],
(profile) => {
async (profile) => {
if (profile.type === "claude") {
throw new Error(message);
}
return Promise.resolve("fallback");
const resolved = await Promise.resolve("fallback");
return resolved;
},
),
),
);
return result;
}),
);
expect(results).toStrictEqual(["fallback", "fallback"]);
@@ -82,14 +86,15 @@ test("falls through after a sign-in failure and records each try", async () => {
{ type: "claude", model: "claude-sonnet-5", token: "first" },
{ type: "opencode", model: "openrouter/qwen/qwen3.8-27b" },
],
(profile) => {
async (profile) => {
if (profile.type === "codex") {
throw new AgentAuthError("codex sign-in failed");
}
if (profile.type === "claude") {
throw new Error("API Error: 429");
}
return Promise.resolve("fallback");
const resolved = await Promise.resolve("fallback");
return resolved;
},
failures,
);
@@ -107,7 +112,7 @@ test("falls through after a sign-in failure and records each try", async () => {
test("throws the last sign-in failure when every profile fails", async () => {
const failures: ProfileFailure[] = [];
await expect(() =>
await expect(
runProfiles(
[
{ type: "codex", model: "gpt-5.6-terra" },
@@ -129,7 +134,7 @@ test("throws the last sign-in failure when every profile fails", async () => {
test("preserves an ordinary agent failure", async () => {
const visited: string[] = [];
await expect(() =>
await expect(
runProfiles(
[
{ type: "claude", model: "claude-sonnet-5", token: "first" },
@@ -146,7 +151,7 @@ test("preserves an ordinary agent failure", async () => {
test("does not mistake incidental quota text for exhaustion", async () => {
const visited: string[] = [];
await expect(() =>
await expect(
runProfiles(
[
{ type: "claude", model: "claude-sonnet-5", token: "first" },
+9 -3
View File
@@ -184,10 +184,12 @@ async function renderPrompt(profile: RuntimeProfile): Promise<RenderedPrompt> {
if (!Array.isArray(comments)) {
throw new TypeError("comments: unexpected Gitea response");
}
const rulesResponse = await gitea(RULES_PATH);
const codeRules = await rulesResponse.text();
const values = new Map<string, string>([
["COMMENT", env("COMMENT")],
["ISSUE_COMMENTS", comments.map((comment: unknown) => formatComment(comment)).join("\n")],
["CODE_RULES", await gitea(RULES_PATH).then((response) => response.text())],
["CODE_RULES", codeRules],
["EVENT_NAME", EVENT],
["GITEA_API_URL", API],
["GITEA_REPOSITORY", REPO],
@@ -432,8 +434,12 @@ async function runOpenCode(
const collected = collectOpenCodeOutput(
child.stdout,
child.stderr,
(chunk) => echo(process.stdout, chunk),
(chunk) => echo(process.stderr, chunk),
async (chunk) => {
await echo(process.stdout, chunk);
},
async (chunk) => {
await echo(process.stderr, chunk);
},
);
const [code, streams] = await Promise.all([status, collected]);
const error = streams.stderr.trim();
+4 -6
View File
@@ -16,11 +16,11 @@ test("encrypts a credential with a fresh supplied nonce", async () => {
});
test("rejects a credential encrypted by another master key", async () => {
await expect(() => decryptCredential(ENCRYPTED, OTHER_KEY)).rejects.toThrow();
await expect(decryptCredential(ENCRYPTED, OTHER_KEY)).rejects.toThrow();
});
test("rejects a credential with a nonstandard nonce", async () => {
await expect(() =>
await expect(
decryptCredential("v1:AA:oV_FAgnsN3p1FyegJxwGVdyjfyPio6qDeSUwbA", MASTER_KEY),
).rejects.toThrow("nonce must be 12 bytes");
});
@@ -57,10 +57,8 @@ test("decrypts the central reviewer token and ordered profiles", async () => {
});
test("rejects malformed central credentials", async () => {
await expect(() => parseCredentials('{"profiles": []}', MASTER_KEY)).rejects.toThrow(
"reviewer-token",
);
await expect(() =>
await expect(parseCredentials('{"profiles": []}', MASTER_KEY)).rejects.toThrow("reviewer-token");
await expect(
parseCredentials(
JSON.stringify({ "reviewer-token": ENCRYPTED, profiles: [{ type: "claude" }] }),
MASTER_KEY,
+10 -3
View File
@@ -33,12 +33,16 @@ function encodeBase64Url(value: Uint8Array): string {
const buffer = (value: Uint8Array): ArrayBuffer => Uint8Array.from(value).buffer;
function importMasterKey(encoded: string, usage: webcrypto.KeyUsage): Promise<webcrypto.CryptoKey> {
async function importMasterKey(
encoded: string,
usage: webcrypto.KeyUsage,
): Promise<webcrypto.CryptoKey> {
const bytes = decodeBase64Url(encoded);
if (bytes.length !== 32) {
throw new Error("master-key must be 32 bytes encoded as base64url");
}
return webcrypto.subtle.importKey("raw", buffer(bytes), "AES-GCM", false, [usage]);
const key = await webcrypto.subtle.importKey("raw", buffer(bytes), "AES-GCM", false, [usage]);
return key;
}
export async function decryptCredential(encrypted: string, masterKey: string): Promise<string> {
@@ -127,7 +131,10 @@ export async function parseCredentials(text: string, masterKey: string): Promise
throw new Error("profiles must contain at least one profile");
}
const parsed = await Promise.all(
profiles.map((entry: unknown, index) => parseProfile(entry, index, masterKey)),
profiles.map(async (entry: unknown, index) => {
const profile = await parseProfile(entry, index, masterKey);
return profile;
}),
);
return { reviewerToken: await decryptCredential(reviewerToken, masterKey), profiles: parsed };
}
+3 -2
View File
@@ -24,8 +24,9 @@ export const IS_ROOT: boolean = currentUid() === 0;
// Symlinks and POSIX modes, as these tests use them, need a Unix host.
export const IS_WINDOWS: boolean = process.platform === "win32";
export function makeTempDir(): Promise<string> {
return mkdtemp(join(tmpdir(), "bot-agents-test-"));
export async function makeTempDir(): Promise<string> {
const path = await mkdtemp(join(tmpdir(), "bot-agents-test-"));
return path;
}
export interface CommandResult {
+17 -3
View File
@@ -40,7 +40,11 @@ const findStep = (steps: Mapping[], key: string, value: string): Mapping => {
test("all committed YAML parses", async () => {
const files = await yamlFiles();
assert(files.length > 0);
await Promise.all(files.map((file) => yaml(file)));
await Promise.all(
files.map(async (file) => {
await yaml(file);
}),
);
});
test("the action exposes only the master key and scopes it to preparation", async () => {
@@ -94,7 +98,12 @@ test("the source protection script stages immutable action files", async () => {
"profiles.json",
"prompt.md",
];
const stats = await Promise.all(files.map((file) => stat(`${path}/${file}`)));
const stats = await Promise.all(
files.map(async (file) => {
const info = await stat(`${path}/${file}`);
return info;
}),
);
for (const [index, info] of stats.entries()) {
assert(info.isFile(), files[index]);
expect(info.mode & 0o022, files[index]).toStrictEqual(0);
@@ -110,7 +119,12 @@ test("module-relative runtime assets exist", async () => {
const assets = [...source.matchAll(/new URL\("(?<asset>[^"]+)", import\.meta\.url\)/gu)].map(
([, asset = ""]) => new URL(asset, module),
);
const stats = await Promise.all(assets.map((asset) => stat(asset)));
const stats = await Promise.all(
assets.map(async (asset) => {
const info = await stat(asset);
return info;
}),
);
for (const [index, info] of stats.entries()) {
assert(info.isFile(), assets[index]?.pathname);
}