feat(khala): fix lint issues, add basic tests

This commit is contained in:
2026-04-25 04:44:13 +00:00
parent c3671d86cf
commit 8e4f191f3f
8 changed files with 79 additions and 51 deletions
+1 -1
View File
@@ -19,7 +19,7 @@
},
"overrides": [
{
"include": ["tsup.config.ts", "*/rslib.config.ts"],
"include": ["tsup.config.ts", "*/rslib.config.ts", "packages/khala/src/index.ts"],
"linter": {
"rules": {
"style": {
@@ -0,0 +1,16 @@
import { describe, it, expect } from "vitest";
import { ok, err } from "../result.js";
describe("Result", () => {
it("ok wraps a value", () => {
const r = ok(42);
expect(r.ok).toBe(true);
if (r.ok) expect(r.value).toBe(42);
});
it("err wraps an error", () => {
const r = err("boom");
expect(r.ok).toBe(false);
if (!r.ok) expect(r.error).toBe("boom");
});
});
+4 -3
View File
@@ -33,9 +33,10 @@ export const agentAuth = createMiddleware<{
return next();
});
export function requireAdmin(
c: { req: { header: (k: string) => string | undefined }; env: KhalaBindings },
): boolean {
export function requireAdmin(c: {
req: { header: (k: string) => string | undefined };
env: KhalaBindings;
}): boolean {
const header = c.req.header("Authorization");
const expected = c.env.ADMIN_SECRET;
if (expected.length === 0) {
+20 -18
View File
@@ -1,7 +1,7 @@
import { monotonicFactory, type ULID } from "ulidx";
import type { Agent, GetThreadMessagesOpts, Message, Task, Thread } from "./types.js";
import { type ULID, monotonicFactory } from "ulidx";
import type { Result } from "./result.js";
import { err, ok } from "./result.js";
import type { Agent, GetThreadMessagesOpts, Message, Task, Thread } from "./types.js";
const generateUlid = monotonicFactory((): number => Date.now());
@@ -70,9 +70,7 @@ export async function setThreadResult(
status: "completed" | "failed",
): Promise<void> {
await db
.prepare(
`UPDATE threads SET result = ?, status = ?, updated_at = datetime('now') WHERE id = ?`,
)
.prepare(`UPDATE threads SET result = ?, status = ?, updated_at = datetime('now') WHERE id = ?`)
.bind(result, status, id)
.run();
}
@@ -98,9 +96,7 @@ export async function appendMessage(
.bind(threadId, role, content, meta, step, agentId)
.run();
const row = await db
.prepare(
`SELECT * FROM messages WHERE thread_id = ? AND step = ? ORDER BY id DESC LIMIT 1`,
)
.prepare("SELECT * FROM messages WHERE thread_id = ? AND step = ? ORDER BY id DESC LIMIT 1")
.bind(threadId, step)
.first<MessageRow>();
if (!row) {
@@ -219,7 +215,10 @@ type TaskRow = {
function rowToTask(row: TaskRow): Task {
const status =
row.status === "open" || row.status === "claimed" || row.status === "completed" || row.status === "expired"
row.status === "open" ||
row.status === "claimed" ||
row.status === "completed" ||
row.status === "expired"
? row.status
: "open";
return {
@@ -265,7 +264,11 @@ export async function getTaskById(db: D1Database, id: string): Promise<Task | nu
return rowToTask(row);
}
export async function completeTask(db: D1Database, taskId: string, claimId: string): Promise<boolean> {
export async function completeTask(
db: D1Database,
taskId: string,
claimId: string,
): Promise<boolean> {
const res = await db
.prepare(
`UPDATE tasks SET status = 'completed' WHERE id = ? AND claim_id = ? AND status = 'claimed'`,
@@ -277,7 +280,11 @@ export async function completeTask(db: D1Database, taskId: string, claimId: stri
return res.success === true;
}
export async function releaseTask(db: D1Database, taskId: string, claimId: string): Promise<boolean> {
export async function releaseTask(
db: D1Database,
taskId: string,
claimId: string,
): Promise<boolean> {
const res = await db
.prepare(
`UPDATE tasks
@@ -314,9 +321,7 @@ export async function getOpenTasks(
const cap = Math.min(500, Math.max(1, limit));
if (workflow === null) {
const st = await db
.prepare(
`SELECT t.* FROM tasks t WHERE t.status = 'open' ORDER BY t.created_at ASC LIMIT ?`,
)
.prepare(`SELECT t.* FROM tasks t WHERE t.status = 'open' ORDER BY t.created_at ASC LIMIT ?`)
.bind(cap)
.all<TaskRow>();
const results = (st.results as TaskRow[] | null) ?? [];
@@ -350,10 +355,7 @@ export async function registerAgent(
.prepare("INSERT INTO agents (id, token_hash, created_at) VALUES (?, ?, datetime('now'))")
.bind(id, tokenHash)
.run();
const row = await db
.prepare("SELECT * FROM agents WHERE id = ?")
.bind(id)
.first<Agent>();
const row = await db.prepare("SELECT * FROM agents WHERE id = ?").bind(id).first<Agent>();
if (!row) return err("registerAgent: insert failed");
return ok(row);
}
+10 -11
View File
@@ -1,18 +1,17 @@
import { Hono } from "hono";
import { agentAuth } from "./auth.js";
import {
claimTask,
createThread,
expireTimedOutTasks,
getOpenTasks,
getTaskById,
getThread,
releaseTask,
} from "./db.js";
import type { KhalaBindings } from "./env.js";
import { admin } from "./routes/admin.js";
import { agentAuth } from "./auth.js";
import { ThreadDO } from "./thread-do.js";
import {
createThread,
getThread,
getThreadMessages,
getOpenTasks,
claimTask,
releaseTask,
getTaskById,
expireTimedOutTasks,
} from "./db.js";
import { getWorkflow } from "./workflows.js";
// Register built-in workflows
+8 -3
View File
@@ -1,8 +1,8 @@
import { Hono } from "hono";
import { requireAdmin, sha256Hex } from "../auth.js";
import { registerAgent, deleteAgent, listAgents } from "../db.js";
import type { Result } from "../result.js";
import { deleteAgent, listAgents, registerAgent } from "../db.js";
import type { KhalaBindings } from "../env.js";
import type { Result } from "../result.js";
const admin = new Hono<{ Bindings: KhalaBindings }>();
@@ -32,7 +32,12 @@ admin.post("/agents", async (c) => {
const body = raw as Readonly<Record<string, unknown>>;
const id = body.id;
const token = body.token;
if (typeof id !== "string" || id.length === 0 || typeof token !== "string" || token.length === 0) {
if (
typeof id !== "string" ||
id.length === 0 ||
typeof token !== "string" ||
token.length === 0
) {
return c.json({ error: "id and token required" }, 400);
}
const hash = await sha256Hex(token);
+20 -13
View File
@@ -34,13 +34,15 @@ function buildSteps(msgs: Awaited<ReturnType<typeof getThreadMessages>>): StepRo
.map((m) => ({
role: m.role,
content: m.content,
meta: m.meta ? (() => {
try {
return JSON.parse(m.meta) as unknown;
} catch {
return m.meta;
}
})() : null,
meta: m.meta
? (() => {
try {
return JSON.parse(m.meta) as unknown;
} catch {
return m.meta;
}
})()
: null,
}));
}
@@ -118,8 +120,16 @@ export class ThreadDO {
const content = body.content;
const claimId = body.claimId;
const agentId = body.agentId;
if (typeof taskId !== "string" || typeof content !== "string" || typeof claimId !== "string" || typeof agentId !== "string") {
return Response.json({ error: "taskId, content, claimId, agentId required" }, { status: 400 });
if (
typeof taskId !== "string" ||
typeof content !== "string" ||
typeof claimId !== "string" ||
typeof agentId !== "string"
) {
return Response.json(
{ error: "taskId, content, claimId, agentId required" },
{ status: 400 },
);
}
const db = this.env.DB;
const task = await getTaskForThreadAndClaim(db, threadId, taskId, claimId);
@@ -157,10 +167,7 @@ export class ThreadDO {
return Response.json({ ok: true });
}
private async listMessages(
url: URL,
threadId: string,
): Promise<Response> {
private async listMessages(url: URL, threadId: string): Promise<Response> {
const roleP = url.searchParams.get("role");
const minStepRaw = url.searchParams.get("minStep");
const maxStepRaw = url.searchParams.get("maxStep");
-2
View File
@@ -13,5 +13,3 @@ declare module "cloudflare:workers" {
}
interface ProvidedEnv extends Env {}
}
export {};