Configure lefthook formatter checks (#231)

* Add lefthook formatter checks

* Fix SDK mode hydration

* Stabilize SDK mode integration test
This commit is contained in:
Nathan Flurry 2026-03-10 23:03:11 -07:00 committed by GitHub
parent 0471214d65
commit d2346bafb3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
282 changed files with 5840 additions and 8399 deletions

View file

@ -21,10 +21,7 @@ describe("OpenCode-compatible Event Streaming", () => {
return `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
}
async function initSessionViaHttp(
sessionId: string,
body: Record<string, unknown>
): Promise<void> {
async function initSessionViaHttp(sessionId: string, body: Record<string, unknown>): Promise<void> {
const response = await fetch(`${handle.baseUrl}/opencode/session/${sessionId}/init`, {
method: "POST",
headers: {
@ -113,10 +110,7 @@ describe("OpenCode-compatible Event Streaming", () => {
for await (const event of (eventStream as any).stream) {
events.push(event);
// Look for message part updates or completion
if (
event.type === "message.part.updated" ||
event.type === "session.idle"
) {
if (event.type === "message.part.updated" || event.type === "session.idle") {
if (events.length >= 3) {
clearTimeout(timeout);
resolve();
@ -175,10 +169,7 @@ describe("OpenCode-compatible Event Streaming", () => {
const statuses: string[] = [];
const collectIdle = new Promise<void>((resolve, reject) => {
const timeout = setTimeout(
() => reject(new Error("Timed out waiting for session.idle")),
15_000
);
const timeout = setTimeout(() => reject(new Error("Timed out waiting for session.idle")), 15_000);
(async () => {
try {
for await (const event of (eventStream as any).stream) {
@ -223,10 +214,7 @@ describe("OpenCode-compatible Event Streaming", () => {
let busySnapshot: string | undefined;
const waitForIdle = new Promise<void>((resolve, reject) => {
const timeout = setTimeout(
() => reject(new Error("Timed out waiting for busy status snapshot + session.idle")),
15_000
);
const timeout = setTimeout(() => reject(new Error("Timed out waiting for busy status snapshot + session.idle")), 15_000);
(async () => {
try {
for await (const event of (eventStream as any).stream) {
@ -276,10 +264,7 @@ describe("OpenCode-compatible Event Streaming", () => {
const idles: any[] = [];
const collectErrorAndIdle = new Promise<void>((resolve, reject) => {
const timeout = setTimeout(
() => reject(new Error("Timed out waiting for session.error + session.idle")),
15_000
);
const timeout = setTimeout(() => reject(new Error("Timed out waiting for session.error + session.idle")), 15_000);
(async () => {
try {
for await (const event of (eventStream as any).stream) {
@ -428,9 +413,7 @@ describe("OpenCode-compatible Event Streaming", () => {
expect(idleEvents.length).toBe(1);
// All tool parts should have been emitted before idle
const toolParts = allEvents.filter(
(e) => e.type === "message.part.updated" && e.properties?.part?.type === "tool"
);
const toolParts = allEvents.filter((e) => e.type === "message.part.updated" && e.properties?.part?.type === "tool");
expect(toolParts.length).toBeGreaterThan(0);
});
@ -459,12 +442,7 @@ describe("OpenCode-compatible Event Streaming", () => {
if (!targetMessageId && partType === "tool" && typeof messageId === "string") {
targetMessageId = messageId;
}
if (
targetMessageId &&
messageId === targetMessageId &&
typeof partId === "string" &&
!seenPartIds.includes(partId)
) {
if (targetMessageId && messageId === targetMessageId && typeof partId === "string" && !seenPartIds.includes(partId)) {
seenPartIds.push(partId);
}
}
@ -497,17 +475,12 @@ describe("OpenCode-compatible Event Streaming", () => {
expect(targetMessageId).toBeTruthy();
expect(seenPartIds.length).toBeGreaterThan(0);
const response = await fetch(
`${handle.baseUrl}/opencode/session/${sessionId}/message/${targetMessageId}`,
{
headers: { Authorization: `Bearer ${handle.token}` },
}
);
const response = await fetch(`${handle.baseUrl}/opencode/session/${sessionId}/message/${targetMessageId}`, {
headers: { Authorization: `Bearer ${handle.token}` },
});
expect(response.ok).toBe(true);
const message = (await response.json()) as any;
const returnedPartIds = (message?.parts ?? [])
.map((part: any) => part?.id)
.filter((id: any) => typeof id === "string");
const returnedPartIds = (message?.parts ?? []).map((part: any) => part?.id).filter((id: any) => typeof id === "string");
const expectedSet = new Set(seenPartIds);
const returnedFiltered = returnedPartIds.filter((id: string) => expectedSet.has(id));

View file

@ -33,10 +33,7 @@ function findBinary(): string | null {
}
// Check cargo build outputs (relative to tests/opencode-compat/helpers)
const cargoPaths = [
resolve(__dirname, "../../../../../../target/debug/sandbox-agent"),
resolve(__dirname, "../../../../../../target/release/sandbox-agent"),
];
const cargoPaths = [resolve(__dirname, "../../../../../../target/debug/sandbox-agent"), resolve(__dirname, "../../../../../../target/release/sandbox-agent")];
for (const p of cargoPaths) {
if (existsSync(p)) {
@ -65,12 +62,7 @@ async function getFreePort(host: string): Promise<number> {
/**
* Wait for the server to become healthy
*/
async function waitForHealth(
baseUrl: string,
token: string,
timeoutMs: number,
child: ChildProcess
): Promise<void> {
async function waitForHealth(baseUrl: string, token: string, timeoutMs: number, child: ChildProcess): Promise<void> {
const start = Date.now();
let lastError: string | undefined;
@ -130,9 +122,7 @@ export interface SpawnOptions {
export async function spawnSandboxAgent(options: SpawnOptions = {}): Promise<SandboxAgentHandle> {
const binaryPath = findBinary();
if (!binaryPath) {
throw new Error(
"sandbox-agent binary not found. Run 'cargo build -p sandbox-agent' first or set SANDBOX_AGENT_BIN."
);
throw new Error("sandbox-agent binary not found. Run 'cargo build -p sandbox-agent' first or set SANDBOX_AGENT_BIN.");
}
const host = options.host ?? "127.0.0.1";
@ -222,7 +212,7 @@ export async function buildSandboxAgent(): Promise<void> {
console.log("Building sandbox-agent...");
const projectRoot = resolve(__dirname, "../../../../../..");
return new Promise((resolve, reject) => {
const proc = spawn("cargo", ["build", "-p", "sandbox-agent"], {
cwd: projectRoot,

View file

@ -60,8 +60,6 @@ describe("OpenCode-compatible Model API", () => {
expect(providerIds.has("claude")).toBe(true);
expect(providerIds.has("codex")).toBe(true);
expect(providerIds.has("pi")).toBe(true);
expect(
providerIds.has("opencode") || Array.from(providerIds).some((id) => id.startsWith("opencode:"))
).toBe(true);
expect(providerIds.has("opencode") || Array.from(providerIds).some((id) => id.startsWith("opencode:"))).toBe(true);
});
});

View file

@ -53,11 +53,7 @@ describe("OpenCode-compatible Permission API", () => {
throw new Error("Timed out waiting for permission request");
}
async function waitForCondition(
check: () => boolean | Promise<boolean>,
timeoutMs = 10_000,
intervalMs = 100,
) {
async function waitForCondition(check: () => boolean | Promise<boolean>, timeoutMs = 10_000, intervalMs = 100) {
const start = Date.now();
while (Date.now() - start < timeoutMs) {
if (await check()) {
@ -68,11 +64,7 @@ describe("OpenCode-compatible Permission API", () => {
throw new Error("Timed out waiting for condition");
}
async function waitForValue<T>(
getValue: () => T | undefined | Promise<T | undefined>,
timeoutMs = 10_000,
intervalMs = 100,
): Promise<T> {
async function waitForValue<T>(getValue: () => T | undefined | Promise<T | undefined>, timeoutMs = 10_000, intervalMs = 100): Promise<T> {
const start = Date.now();
while (Date.now() - start < timeoutMs) {
const value = await getValue();
@ -175,13 +167,7 @@ describe("OpenCode-compatible Permission API", () => {
});
expect(firstReply.error).toBeUndefined();
await waitForCondition(() =>
repliedEvents.some(
(event) =>
event?.properties?.requestID === firstRequestId &&
event?.properties?.reply === "always",
),
);
await waitForCondition(() => repliedEvents.some((event) => event?.properties?.requestID === firstRequestId && event?.properties?.reply === "always"));
await client.session.prompt({
sessionID: sessionId,
@ -190,11 +176,7 @@ describe("OpenCode-compatible Permission API", () => {
});
const autoReplyEvent = await waitForValue(() =>
repliedEvents.find(
(event) =>
event?.properties?.requestID !== firstRequestId &&
event?.properties?.reply === "always",
),
repliedEvents.find((event) => event?.properties?.requestID !== firstRequestId && event?.properties?.reply === "always"),
);
const autoRequestId = autoReplyEvent?.properties?.requestID;
expect(autoRequestId).toBeDefined();

View file

@ -15,11 +15,7 @@
import { describe, it, expect, beforeAll, beforeEach, afterEach } from "vitest";
import { createOpencodeClient, type OpencodeClient } from "@opencode-ai/sdk";
import {
spawnSandboxAgent,
buildSandboxAgent,
type SandboxAgentHandle,
} from "./helpers/spawn";
import { spawnSandboxAgent, buildSandboxAgent, type SandboxAgentHandle } from "./helpers/spawn";
const MODEL = process.env.TEST_AGENT_MODEL;
@ -62,22 +58,11 @@ describe.skipIf(!MODEL)("Real agent round-trip", () => {
* Uses a manual iterator to avoid closing the stream (for-await-of calls
* iterator.return() on early exit, which would close the SSE connection).
*/
function collectUntilIdle(
iter: AsyncIterator<any>,
timeoutMs = 30_000,
): Promise<{ events: any[]; text: string }> {
function collectUntilIdle(iter: AsyncIterator<any>, timeoutMs = 30_000): Promise<{ events: any[]; text: string }> {
const events: any[] = [];
let text = "";
return new Promise((resolve, reject) => {
const timeout = setTimeout(
() =>
reject(
new Error(
`Timed out after ${timeoutMs}ms. Events: ${JSON.stringify(events.map((e) => e.type))}`,
),
),
timeoutMs,
);
const timeout = setTimeout(() => reject(new Error(`Timed out after ${timeoutMs}ms. Events: ${JSON.stringify(events.map((e) => e.type))}`)), timeoutMs);
(async () => {
try {
while (true) {
@ -88,10 +73,7 @@ describe.skipIf(!MODEL)("Real agent round-trip", () => {
return;
}
events.push(event);
if (
event.type === "message.part.updated" &&
event.properties?.part?.type === "text"
) {
if (event.type === "message.part.updated" && event.properties?.part?.type === "text") {
// Prefer the delta (chunk) if present; otherwise use the full
// accumulated part.text (for non-streaming single-shot events).
text += event.properties.delta ?? event.properties.part.text ?? "";
@ -103,11 +85,7 @@ describe.skipIf(!MODEL)("Real agent round-trip", () => {
}
if (event.type === "session.error") {
clearTimeout(timeout);
reject(
new Error(
`session.error: ${JSON.stringify(event.properties?.error)}`,
),
);
reject(new Error(`session.error: ${JSON.stringify(event.properties?.error)}`));
return;
}
}

View file

@ -55,10 +55,7 @@ describe("OpenCode-compatible Session API", () => {
return (sessions ?? []).find((item: any) => item.id === sessionId);
}
async function initSessionViaHttp(
sessionId: string,
body: Record<string, unknown> = {}
): Promise<{ response: Response; data: any }> {
async function initSessionViaHttp(sessionId: string, body: Record<string, unknown> = {}): Promise<{ response: Response; data: any }> {
const response = await fetch(`${handle.baseUrl}/opencode/session/${sessionId}/init`, {
method: "POST",
headers: {
@ -300,7 +297,7 @@ describe("OpenCode-compatible Session API", () => {
});
expect(changed.response.status).toBe(400);
expect(changed.data?.errors?.[0]?.message).toBe(
"OpenCode compatibility currently does not support changing the model after creating a session. Export with /export and load in to a new session."
"OpenCode compatibility currently does not support changing the model after creating a session. Export with /export and load in to a new session.",
);
});
@ -337,9 +334,7 @@ describe("OpenCode-compatible Session API", () => {
it("should keep session.get available during first prompt after /new-style creation", async () => {
const providers = await getProvidersViaHttp();
const providerId = providers.connected.find(
(provider) => provider !== "mock" && typeof providers.default?.[provider] === "string"
);
const providerId = providers.connected.find((provider) => provider !== "mock" && typeof providers.default?.[provider] === "string");
if (!providerId) {
return;
}
@ -478,7 +473,7 @@ describe("OpenCode-compatible Session API", () => {
expect(response.status).toBe(400);
expect(data?.errors?.[0]?.message).toBe(
"OpenCode compatibility currently does not support changing the model after creating a session. Export with /export and load in to a new session."
"OpenCode compatibility currently does not support changing the model after creating a session. Export with /export and load in to a new session.",
);
}
});
@ -511,7 +506,7 @@ describe("OpenCode-compatible Session API", () => {
expect(response.status).toBe(400);
expect(data?.errors?.[0]?.message).toBe(
"OpenCode compatibility currently does not support changing the model after creating a session. Export with /export and load in to a new session."
"OpenCode compatibility currently does not support changing the model after creating a session. Export with /export and load in to a new session.",
);
});
});