feat(khala): fix lint issues, add basic tests
This commit is contained in:
+1
-1
@@ -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");
|
||||
});
|
||||
});
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
@@ -13,5 +13,3 @@ declare module "cloudflare:workers" {
|
||||
}
|
||||
interface ProvidedEnv extends Env {}
|
||||
}
|
||||
|
||||
export {};
|
||||
|
||||
Reference in New Issue
Block a user