Turn on promise-function-async and init-declarations and satisfy them with async functions and null-initialized lets
This commit is contained in:
+13
-12
@@ -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
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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
@@ -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();
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user