Skip to content

Commit 514bea7

Browse files
authored
Merge pull request #1 from OpenKnots/okcode/codebase-improvements
Add CI coverage and route error boundaries
2 parents 7147978 + 13584de commit 514bea7

37 files changed

Lines changed: 1694 additions & 44 deletions

.github/workflows/ci.yml

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
branches: [main]
8+
9+
jobs:
10+
check:
11+
runs-on: ubuntu-latest
12+
13+
steps:
14+
- uses: actions/checkout@v4
15+
16+
- uses: pnpm/action-setup@v4
17+
18+
- uses: actions/setup-node@v4
19+
with:
20+
node-version: 18
21+
cache: pnpm
22+
23+
- run: pnpm install --frozen-lockfile
24+
25+
- run: pnpm exec secretlint "**/*"
26+
27+
- run: pnpm test -- --run

__tests__/api-contract.test.ts

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import { describe, it, expect } from "vitest";
2+
import { ok, fail, ApiValidationError } from "@/lib/opentrust/api-contract";
3+
4+
describe("ok()", () => {
5+
it("wraps data in success envelope", () => {
6+
const result = ok({ foo: "bar" });
7+
expect(result.ok).toBe(true);
8+
expect(result.data).toEqual({ foo: "bar" });
9+
});
10+
11+
it("works with null data", () => {
12+
const result = ok(null);
13+
expect(result.ok).toBe(true);
14+
expect(result.data).toBeNull();
15+
});
16+
});
17+
18+
describe("fail()", () => {
19+
it("formats ApiValidationError with code and message", () => {
20+
const error = new ApiValidationError("Bad input", { code: "invalid_input", status: 400 });
21+
const result = fail(error);
22+
23+
expect(result.ok).toBe(false);
24+
expect(result.error.code).toBe("invalid_input");
25+
expect(result.error.message).toBe("Bad input");
26+
expect(result.status).toBe(400);
27+
});
28+
29+
it("includes details when provided", () => {
30+
const error = new ApiValidationError("Missing field", {
31+
code: "missing_field",
32+
details: { field: "title" },
33+
});
34+
const result = fail(error);
35+
expect(result.error.details).toEqual({ field: "title" });
36+
});
37+
38+
it("returns internal_error for unknown errors", () => {
39+
const result = fail(new Error("something broke"));
40+
expect(result.ok).toBe(false);
41+
expect(result.error.code).toBe("internal_error");
42+
expect(result.status).toBe(500);
43+
// Should NOT leak the original error message
44+
expect(result.error.message).not.toContain("something broke");
45+
});
46+
47+
it("handles non-Error thrown values", () => {
48+
const result = fail("string error");
49+
expect(result.ok).toBe(false);
50+
expect(result.error.code).toBe("internal_error");
51+
});
52+
});
53+
54+
describe("ApiValidationError", () => {
55+
it("defaults to status 400 and code invalid_request", () => {
56+
const error = new ApiValidationError("test");
57+
expect(error.status).toBe(400);
58+
expect(error.code).toBe("invalid_request");
59+
expect(error.name).toBe("ApiValidationError");
60+
});
61+
62+
it("accepts custom status and code", () => {
63+
const error = new ApiValidationError("forbidden", { status: 403, code: "forbidden" });
64+
expect(error.status).toBe(403);
65+
expect(error.code).toBe("forbidden");
66+
});
67+
});

__tests__/artifact-extract.test.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { describe, it, expect } from "vitest";
2+
import { extractArtifactsFromText } from "@/lib/opentrust/artifact-extract";
3+
4+
describe("extractArtifactsFromText", () => {
5+
it("extracts URLs", () => {
6+
const text = "Check out https://example.com/docs and http://localhost:3000";
7+
const artifacts = extractArtifactsFromText(text);
8+
9+
const urls = artifacts.filter((a) => a.kind === "url");
10+
expect(urls.length).toBeGreaterThanOrEqual(2);
11+
expect(urls.some((a) => a.uri.includes("example.com"))).toBe(true);
12+
});
13+
14+
it("extracts GitHub-style repo references", () => {
15+
const text = "See OpenKnots/OpenTrust for details";
16+
const artifacts = extractArtifactsFromText(text);
17+
18+
const repos = artifacts.filter((a) => a.kind === "repo");
19+
expect(repos.some((a) => a.uri === "OpenKnots/OpenTrust")).toBe(true);
20+
});
21+
22+
it("extracts doc file references", () => {
23+
const text = "Updated docs/ARCHITECTURE.md and lib/opentrust/db.ts";
24+
const artifacts = extractArtifactsFromText(text);
25+
26+
const docs = artifacts.filter((a) => a.kind === "doc");
27+
expect(docs.some((a) => a.uri.includes("ARCHITECTURE.md"))).toBe(true);
28+
expect(docs.some((a) => a.uri.includes("db.ts"))).toBe(true);
29+
});
30+
31+
it("deduplicates artifacts by kind+uri", () => {
32+
const text = "See https://example.com and also https://example.com again";
33+
const artifacts = extractArtifactsFromText(text);
34+
35+
const urls = artifacts.filter((a) => a.kind === "url" && a.uri.includes("example.com"));
36+
expect(urls).toHaveLength(1);
37+
});
38+
39+
it("limits to 24 artifacts", () => {
40+
const urls = Array.from({ length: 30 }, (_, i) => `https://example.com/page${i}`).join(" ");
41+
const artifacts = extractArtifactsFromText(urls);
42+
expect(artifacts.length).toBeLessThanOrEqual(24);
43+
});
44+
45+
it("returns empty for text with no artifacts", () => {
46+
const artifacts = extractArtifactsFromText("Just plain text with no links or references");
47+
// May or may not have matches depending on regex — at minimum should not throw
48+
expect(Array.isArray(artifacts)).toBe(true);
49+
});
50+
});

__tests__/auth-rate-limit.test.ts

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import { describe, it, expect, beforeEach } from "vitest";
2+
import {
3+
getLoginRateLimit,
4+
recordLoginFailure,
5+
clearLoginFailures,
6+
getRateLimitKey,
7+
} from "@/lib/opentrust/auth-rate-limit";
8+
9+
describe("getRateLimitKey", () => {
10+
it("returns the IP when provided", () => {
11+
expect(getRateLimitKey("192.168.1.1")).toBe("192.168.1.1");
12+
});
13+
14+
it("trims whitespace", () => {
15+
expect(getRateLimitKey(" 10.0.0.1 ")).toBe("10.0.0.1");
16+
});
17+
18+
it('returns "unknown" for null/undefined/empty', () => {
19+
expect(getRateLimitKey(null)).toBe("unknown");
20+
expect(getRateLimitKey(undefined)).toBe("unknown");
21+
expect(getRateLimitKey("")).toBe("unknown");
22+
});
23+
});
24+
25+
describe("getLoginRateLimit", () => {
26+
beforeEach(() => {
27+
clearLoginFailures("test-ip-limit");
28+
});
29+
30+
it("starts with 5 remaining attempts", () => {
31+
const state = getLoginRateLimit("test-ip-limit");
32+
expect(state.remaining).toBe(5);
33+
expect(state.count).toBe(0);
34+
});
35+
36+
it("returns a future resetAt timestamp", () => {
37+
const state = getLoginRateLimit("test-ip-limit");
38+
expect(state.resetAt).toBeGreaterThan(Date.now());
39+
});
40+
});
41+
42+
describe("recordLoginFailure", () => {
43+
beforeEach(() => {
44+
clearLoginFailures("test-ip-fail");
45+
});
46+
47+
it("increments the failure count", () => {
48+
recordLoginFailure("test-ip-fail");
49+
const state = getLoginRateLimit("test-ip-fail");
50+
expect(state.remaining).toBe(4);
51+
});
52+
53+
it("marks as limited after 5 failures", () => {
54+
for (let i = 0; i < 4; i++) {
55+
const result = recordLoginFailure("test-ip-fail");
56+
expect(result.limited).toBe(false);
57+
}
58+
const fifth = recordLoginFailure("test-ip-fail");
59+
expect(fifth.limited).toBe(true);
60+
});
61+
62+
it("returns 0 remaining after limit reached", () => {
63+
for (let i = 0; i < 5; i++) {
64+
recordLoginFailure("test-ip-fail");
65+
}
66+
const state = getLoginRateLimit("test-ip-fail");
67+
expect(state.remaining).toBe(0);
68+
});
69+
});
70+
71+
describe("clearLoginFailures", () => {
72+
it("resets the failure count", () => {
73+
recordLoginFailure("test-ip-clear");
74+
recordLoginFailure("test-ip-clear");
75+
clearLoginFailures("test-ip-clear");
76+
77+
const state = getLoginRateLimit("test-ip-clear");
78+
expect(state.remaining).toBe(5);
79+
});
80+
});

__tests__/db.test.ts

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
import { describe, it, expect, beforeEach, afterEach } from "vitest";
2+
import Database from "better-sqlite3";
3+
import { existsSync, mkdirSync, readFileSync, rmSync } from "node:fs";
4+
import path from "node:path";
5+
6+
/**
7+
* These tests exercise the database layer against a real SQLite instance
8+
* using a temporary directory, bypassing the singleton to avoid polluting
9+
* the development database.
10+
*/
11+
12+
const TEST_DIR = path.join(process.cwd(), "storage", "__test__");
13+
const TEST_DB_PATH = path.join(TEST_DIR, "test.sqlite");
14+
const MIGRATION_PATH = path.join(process.cwd(), "db", "0001_init.sql");
15+
16+
function freshDb() {
17+
if (!existsSync(TEST_DIR)) mkdirSync(TEST_DIR, { recursive: true });
18+
if (existsSync(TEST_DB_PATH)) rmSync(TEST_DB_PATH);
19+
20+
const db = new Database(TEST_DB_PATH);
21+
db.exec("PRAGMA journal_mode=WAL;");
22+
db.exec("PRAGMA foreign_keys=ON;");
23+
return db;
24+
}
25+
26+
describe("database initialization", () => {
27+
let db: Database.Database;
28+
29+
beforeEach(() => {
30+
db = freshDb();
31+
});
32+
33+
afterEach(() => {
34+
db.close();
35+
if (existsSync(TEST_DB_PATH)) rmSync(TEST_DB_PATH);
36+
});
37+
38+
it("enables WAL journal mode", () => {
39+
const row = db.prepare("PRAGMA journal_mode;").get() as { journal_mode: string };
40+
expect(row.journal_mode).toBe("wal");
41+
});
42+
43+
it("enables foreign keys", () => {
44+
const row = db.prepare("PRAGMA foreign_keys;").get() as { foreign_keys: number };
45+
expect(row.foreign_keys).toBe(1);
46+
});
47+
48+
it("applies the init migration without errors", () => {
49+
const migrationSql = readFileSync(MIGRATION_PATH, "utf8");
50+
expect(() => db.exec(migrationSql)).not.toThrow();
51+
});
52+
53+
it("creates expected core tables after migration", () => {
54+
const migrationSql = readFileSync(MIGRATION_PATH, "utf8");
55+
db.exec(migrationSql);
56+
57+
const tables = db
58+
.prepare("SELECT name FROM sqlite_master WHERE type = 'table' ORDER BY name;")
59+
.all() as { name: string }[];
60+
61+
const tableNames = tables.map((t) => t.name);
62+
63+
expect(tableNames).toContain("sessions");
64+
expect(tableNames).toContain("traces");
65+
expect(tableNames).toContain("events");
66+
expect(tableNames).toContain("tool_calls");
67+
expect(tableNames).toContain("workflow_runs");
68+
expect(tableNames).toContain("workflow_steps");
69+
expect(tableNames).toContain("artifacts");
70+
expect(tableNames).toContain("capabilities");
71+
});
72+
73+
it("migration is idempotent — running twice does not error", () => {
74+
const migrationSql = readFileSync(MIGRATION_PATH, "utf8");
75+
db.exec(migrationSql);
76+
expect(() => db.exec(migrationSql)).not.toThrow();
77+
});
78+
});
79+
80+
describe("transaction support", () => {
81+
let db: Database.Database;
82+
83+
beforeEach(() => {
84+
db = freshDb();
85+
const migrationSql = readFileSync(MIGRATION_PATH, "utf8");
86+
db.exec(migrationSql);
87+
});
88+
89+
afterEach(() => {
90+
db.close();
91+
if (existsSync(TEST_DB_PATH)) rmSync(TEST_DB_PATH);
92+
});
93+
94+
it("commits all writes on success", () => {
95+
const run = db.transaction(() => {
96+
db.prepare(
97+
"INSERT INTO capabilities (id, kind, name, metadata_json) VALUES (?, ?, ?, ?)",
98+
).run("test:a", "skill", "a", "{}");
99+
db.prepare(
100+
"INSERT INTO capabilities (id, kind, name, metadata_json) VALUES (?, ?, ?, ?)",
101+
).run("test:b", "skill", "b", "{}");
102+
});
103+
104+
run();
105+
106+
const rows = db.prepare("SELECT id FROM capabilities WHERE id LIKE 'test:%'").all() as { id: string }[];
107+
expect(rows).toHaveLength(2);
108+
});
109+
110+
it("rolls back all writes on failure", () => {
111+
const run = db.transaction(() => {
112+
db.prepare(
113+
"INSERT INTO capabilities (id, kind, name, metadata_json) VALUES (?, ?, ?, ?)",
114+
).run("test:c", "skill", "c", "{}");
115+
throw new Error("deliberate failure");
116+
});
117+
118+
expect(() => run()).toThrow("deliberate failure");
119+
120+
const rows = db.prepare("SELECT id FROM capabilities WHERE id = 'test:c'").all();
121+
expect(rows).toHaveLength(0);
122+
});
123+
});

0 commit comments

Comments
 (0)