mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-04-27 17:36:25 +02:00
strengthen repo verification and runtime coverage
Add clearer app docs plus targeted desktop, CLI, web, and worker tests so cross-surface regressions are caught earlier and the repo is easier to navigate.
This commit is contained in:
parent
2133d7226f
commit
4239f9f1ef
63 changed files with 3678 additions and 764 deletions
83
apps/cli/test/repos.test.mjs
Normal file
83
apps/cli/test/repos.test.mjs
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
|
||||
import { WorkDir } from "../dist/config/config.js";
|
||||
import { FSModelConfigRepo } from "../dist/models/repo.js";
|
||||
import { FSMcpConfigRepo } from "../dist/mcp/repo.js";
|
||||
import { FSAgentsRepo } from "../dist/agents/repo.js";
|
||||
import { FSRunsRepo } from "../dist/runs/repo.js";
|
||||
|
||||
test("uses ROWBOAT_WORKDIR override and eagerly creates expected directories", async () => {
|
||||
assert.equal(WorkDir, process.env.ROWBOAT_WORKDIR);
|
||||
|
||||
for (const dirName of ["agents", "config", "runs"]) {
|
||||
const stats = await fs.stat(path.join(WorkDir, dirName));
|
||||
assert.equal(stats.isDirectory(), true);
|
||||
}
|
||||
});
|
||||
|
||||
test("FSModelConfigRepo returns defaults on a fresh workspace", async () => {
|
||||
const repo = new FSModelConfigRepo();
|
||||
const config = await repo.getConfig();
|
||||
|
||||
assert.equal(config.defaults.provider, "openai");
|
||||
assert.equal(config.defaults.model, "gpt-5.1");
|
||||
assert.equal(config.providers.openai?.flavor, "openai");
|
||||
});
|
||||
|
||||
test("FSMcpConfigRepo returns an empty config on a fresh workspace", async () => {
|
||||
const repo = new FSMcpConfigRepo();
|
||||
const config = await repo.getConfig();
|
||||
|
||||
assert.deepEqual(config, { mcpServers: {} });
|
||||
});
|
||||
|
||||
test("FSAgentsRepo can create and read nested agent files", async () => {
|
||||
const repo = new FSAgentsRepo();
|
||||
await repo.create({
|
||||
name: "team/copilot",
|
||||
description: "Team helper",
|
||||
provider: "openai",
|
||||
model: "gpt-5.1",
|
||||
instructions: "Be helpful.",
|
||||
});
|
||||
|
||||
const fetched = await repo.fetch("team/copilot");
|
||||
assert.equal(fetched.name, "team/copilot");
|
||||
assert.equal(fetched.description, "Team helper");
|
||||
assert.equal(fetched.instructions, "Be helpful.");
|
||||
});
|
||||
|
||||
test("FSRunsRepo creates, fetches, and lists runs", async () => {
|
||||
let nextId = 0;
|
||||
const repo = new FSRunsRepo({
|
||||
idGenerator: {
|
||||
next: async () => `run-${++nextId}`,
|
||||
},
|
||||
});
|
||||
|
||||
const first = await repo.create({ agentId: "copilot" });
|
||||
await repo.appendEvents(first.id, [{
|
||||
type: "message",
|
||||
runId: first.id,
|
||||
subflow: [],
|
||||
messageId: "msg-1",
|
||||
message: {
|
||||
role: "user",
|
||||
content: "hello",
|
||||
},
|
||||
}]);
|
||||
|
||||
const second = await repo.create({ agentId: "planner" });
|
||||
|
||||
const fetched = await repo.fetch(first.id);
|
||||
assert.equal(fetched.id, first.id);
|
||||
assert.equal(fetched.agentId, "copilot");
|
||||
assert.equal(fetched.log.length, 2);
|
||||
assert.equal(fetched.log[1].type, "message");
|
||||
|
||||
const listed = await repo.list();
|
||||
assert.deepEqual(listed.runs.map((run) => run.id), [second.id, first.id]);
|
||||
});
|
||||
31
apps/cli/test/run-tests.mjs
Normal file
31
apps/cli/test/run-tests.mjs
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
import { mkdtemp, rm } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import path from "node:path";
|
||||
import { spawn } from "node:child_process";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
const packageDir = path.resolve(__dirname, "..");
|
||||
const tempRoot = await mkdtemp(path.join(tmpdir(), "rowboat-cli-test-"));
|
||||
const testWorkDir = path.join(tempRoot, "workspace");
|
||||
|
||||
try {
|
||||
const exitCode = await new Promise((resolve, reject) => {
|
||||
const child = spawn(process.execPath, ["--test", "./test/repos.test.mjs", "./test/server.test.mjs"], {
|
||||
cwd: packageDir,
|
||||
stdio: "inherit",
|
||||
env: {
|
||||
...process.env,
|
||||
ROWBOAT_WORKDIR: testWorkDir,
|
||||
},
|
||||
});
|
||||
|
||||
child.on("error", reject);
|
||||
child.on("exit", (code) => resolve(code ?? 1));
|
||||
});
|
||||
|
||||
process.exitCode = Number(exitCode);
|
||||
} finally {
|
||||
await rm(tempRoot, { recursive: true, force: true });
|
||||
}
|
||||
131
apps/cli/test/server.test.mjs
Normal file
131
apps/cli/test/server.test.mjs
Normal file
|
|
@ -0,0 +1,131 @@
|
|||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import { createApp } from "../dist/server.js";
|
||||
|
||||
test("message endpoint creates a message and returns its id", async () => {
|
||||
const calls = [];
|
||||
const app = createApp({
|
||||
createMessage: async (runId, message) => {
|
||||
calls.push({ runId, message });
|
||||
return "msg-123";
|
||||
},
|
||||
authorizePermission: async () => {},
|
||||
replyToHumanInputRequest: async () => {},
|
||||
stop: async () => {},
|
||||
subscribeToEvents: async () => () => {},
|
||||
});
|
||||
|
||||
const response = await app.request("/runs/run-1/messages/new", {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify({ message: "hello" }),
|
||||
});
|
||||
|
||||
assert.equal(response.status, 200);
|
||||
assert.deepEqual(await response.json(), { messageId: "msg-123" });
|
||||
assert.deepEqual(calls, [{ runId: "run-1", message: "hello" }]);
|
||||
});
|
||||
|
||||
test("permission endpoint validates payload and calls dependency", async () => {
|
||||
const calls = [];
|
||||
const app = createApp({
|
||||
createMessage: async () => "unused",
|
||||
authorizePermission: async (runId, payload) => {
|
||||
calls.push({ runId, payload });
|
||||
},
|
||||
replyToHumanInputRequest: async () => {},
|
||||
stop: async () => {},
|
||||
subscribeToEvents: async () => () => {},
|
||||
});
|
||||
|
||||
const response = await app.request("/runs/run-2/permissions/authorize", {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
subflow: ["child"],
|
||||
toolCallId: "tool-1",
|
||||
response: "approve",
|
||||
}),
|
||||
});
|
||||
|
||||
assert.equal(response.status, 200);
|
||||
assert.deepEqual(await response.json(), { success: true });
|
||||
assert.deepEqual(calls, [{
|
||||
runId: "run-2",
|
||||
payload: {
|
||||
subflow: ["child"],
|
||||
toolCallId: "tool-1",
|
||||
response: "approve",
|
||||
},
|
||||
}]);
|
||||
});
|
||||
|
||||
test("invalid message payload returns a validation error", async () => {
|
||||
const app = createApp({
|
||||
createMessage: async () => "unused",
|
||||
authorizePermission: async () => {},
|
||||
replyToHumanInputRequest: async () => {},
|
||||
stop: async () => {},
|
||||
subscribeToEvents: async () => () => {},
|
||||
});
|
||||
|
||||
const response = await app.request("/runs/run-1/messages/new", {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify({}),
|
||||
});
|
||||
|
||||
assert.equal(response.status, 400);
|
||||
});
|
||||
|
||||
test("openapi endpoint is exposed", async () => {
|
||||
const app = createApp({
|
||||
createMessage: async () => "unused",
|
||||
authorizePermission: async () => {},
|
||||
replyToHumanInputRequest: async () => {},
|
||||
stop: async () => {},
|
||||
subscribeToEvents: async () => () => {},
|
||||
});
|
||||
|
||||
const response = await app.request("/openapi.json");
|
||||
const body = await response.json();
|
||||
|
||||
assert.equal(response.status, 200);
|
||||
assert.equal(body.info.title, "Hono");
|
||||
assert.ok(body.paths["/runs/{runId}/messages/new"]);
|
||||
});
|
||||
|
||||
test("stream endpoint emits SSE payloads and unsubscribes on cancel", async () => {
|
||||
let listener;
|
||||
let unsubscribed = false;
|
||||
const app = createApp({
|
||||
createMessage: async () => "unused",
|
||||
authorizePermission: async () => {},
|
||||
replyToHumanInputRequest: async () => {},
|
||||
stop: async () => {},
|
||||
subscribeToEvents: async (fn) => {
|
||||
listener = fn;
|
||||
return () => {
|
||||
unsubscribed = true;
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
const response = await app.request("/stream");
|
||||
assert.equal(response.status, 200);
|
||||
assert.equal(response.headers.get("content-type"), "text/event-stream");
|
||||
|
||||
await listener({ type: "message", data: { hello: "world" } });
|
||||
|
||||
const reader = response.body.getReader();
|
||||
const chunk = await reader.read();
|
||||
const text = new TextDecoder().decode(chunk.value);
|
||||
|
||||
assert.match(text, /event: message/);
|
||||
assert.match(text, /"hello":"world"/);
|
||||
|
||||
await reader.cancel();
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
assert.equal(unsubscribed, true);
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue