mirror of
https://github.com/different-ai/openwork
synced 2026-04-25 17:15:34 +02:00
* feat(server-v2): add standalone runtime and SDK foundation * docs(server-v2): drop planning task checklists * build(server-v2): generate OpenAPI and SDK during dev * build(server-v2): generate API artifacts before builds * build(server-v2): drop duplicate root SDK generation * build(app): remove SDK generation hooks --------- Co-authored-by: src-opn <src-opn@users.noreply.github.com>
351 lines
16 KiB
TypeScript
351 lines
16 KiB
TypeScript
import { afterEach, expect, test } from "bun:test";
|
|
import fs from "node:fs";
|
|
import path from "node:path";
|
|
import os from "node:os";
|
|
import { createApp } from "./app.js";
|
|
import { createAppDependencies } from "./context/app-dependencies.js";
|
|
|
|
const tempRoots: string[] = [];
|
|
|
|
afterEach(() => {
|
|
while (tempRoots.length) {
|
|
const next = tempRoots.pop();
|
|
if (!next) continue;
|
|
fs.rmSync(next, { force: true, recursive: true });
|
|
}
|
|
});
|
|
|
|
function createTempRoot(label: string) {
|
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), `${label}-`));
|
|
tempRoots.push(root);
|
|
return root;
|
|
}
|
|
|
|
function createTestApp() {
|
|
const root = createTempRoot("openwork-server-v2-phase7");
|
|
const dependencies = createAppDependencies({
|
|
environment: "test",
|
|
inMemory: true,
|
|
runtime: {
|
|
bootstrapPolicy: "disabled",
|
|
},
|
|
startedAt: new Date("2026-04-14T00:00:00.000Z"),
|
|
version: "0.0.0-test",
|
|
workingDirectory: path.join(root, "server-v2"),
|
|
});
|
|
|
|
return {
|
|
app: createApp({ dependencies }),
|
|
dependencies,
|
|
root,
|
|
};
|
|
}
|
|
|
|
test("local workspace creation and config routes use server-owned config directories", async () => {
|
|
const { app, dependencies, root } = createTestApp();
|
|
const workspaceRoot = path.join(root, "workspace-alpha");
|
|
|
|
const createResponse = await app.request("http://openwork.local/workspaces/local", {
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
},
|
|
body: JSON.stringify({ folderPath: workspaceRoot, name: "Alpha", preset: "starter" }),
|
|
});
|
|
const created = await createResponse.json();
|
|
const workspaceId = created.data.id as string;
|
|
|
|
expect(createResponse.status).toBe(200);
|
|
expect(created.data.backend.local.dataDir).toBe(workspaceRoot);
|
|
expect(created.data.backend.local.configDir).toContain(`/workspaces/${workspaceId}/config`);
|
|
|
|
const configResponse = await app.request(`http://openwork.local/workspaces/${workspaceId}/config`);
|
|
const configBody = await configResponse.json();
|
|
expect(configResponse.status).toBe(200);
|
|
expect(configBody.data.stored.openwork.authorizedRoots).toEqual([]);
|
|
expect(configBody.data.effective.openwork.authorizedRoots).toEqual([]);
|
|
expect(configBody.data.effective.opencode.permission?.external_directory).toBeUndefined();
|
|
|
|
const patchResponse = await app.request(`http://openwork.local/workspaces/${workspaceId}/config`, {
|
|
method: "PATCH",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({
|
|
openwork: { reload: { auto: true } },
|
|
opencode: { permission: { external_directory: { [`${path.join(root, "shared-data")}/*`]: "allow" } } },
|
|
}),
|
|
});
|
|
const patched = await patchResponse.json();
|
|
expect(patchResponse.status).toBe(200);
|
|
expect(patched.data.stored.openwork.reload.auto).toBe(true);
|
|
expect(patched.data.stored.openwork.authorizedRoots).toEqual([]);
|
|
expect(patched.data.effective.openwork.authorizedRoots).toEqual([path.join(root, "shared-data")]);
|
|
expect(patched.data.effective.opencode.permission.external_directory[`${path.join(root, "shared-data")}/*`]).toBe("allow");
|
|
expect(patched.data.effective.opencode.permission.external_directory[`${workspaceRoot}/*`]).toBeUndefined();
|
|
|
|
const rawResponse = await app.request(`http://openwork.local/workspaces/${workspaceId}/config/opencode-raw?scope=project`);
|
|
const rawBody = await rawResponse.json();
|
|
expect(rawResponse.status).toBe(200);
|
|
expect(rawBody.data.content).toContain("external_directory");
|
|
expect(rawBody.data.path).toContain(`/workspaces/${workspaceId}/config/opencode.jsonc`);
|
|
|
|
const persistedWorkspace = dependencies.persistence.repositories.workspaces.getById(workspaceId);
|
|
expect(persistedWorkspace?.configDir).toBeTruthy();
|
|
expect(fs.existsSync(path.join(persistedWorkspace!.configDir!, "opencode.jsonc"))).toBe(true);
|
|
expect(fs.existsSync(path.join(workspaceRoot, "opencode.jsonc"))).toBe(true);
|
|
});
|
|
|
|
test("file routes cover simple content, file sessions, inbox, artifacts, and reload events", async () => {
|
|
const { app, root } = createTestApp();
|
|
const workspaceRoot = path.join(root, "workspace-beta");
|
|
|
|
const createResponse = await app.request("http://openwork.local/workspaces/local", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ folderPath: workspaceRoot, name: "Beta", preset: "starter" }),
|
|
});
|
|
const created = await createResponse.json();
|
|
const workspaceId = created.data.id as string;
|
|
|
|
const contentWrite = await app.request(`http://openwork.local/workspaces/${workspaceId}/files/content`, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ path: "notes/today.md", content: "hello phase 7" }),
|
|
});
|
|
const contentWriteBody = await contentWrite.json();
|
|
expect(contentWrite.status).toBe(200);
|
|
expect(contentWriteBody.data.path).toBe("notes/today.md");
|
|
|
|
const contentRead = await app.request(`http://openwork.local/workspaces/${workspaceId}/files/content?path=notes/today.md`);
|
|
const contentReadBody = await contentRead.json();
|
|
expect(contentRead.status).toBe(200);
|
|
expect(contentReadBody.data.content).toBe("hello phase 7");
|
|
|
|
const sessionCreate = await app.request(`http://openwork.local/workspaces/${workspaceId}/file-sessions`, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ write: true }),
|
|
});
|
|
const sessionBody = await sessionCreate.json();
|
|
const fileSessionId = sessionBody.data.id as string;
|
|
expect(sessionCreate.status).toBe(200);
|
|
|
|
const writeBatch = await app.request(`http://openwork.local/workspaces/${workspaceId}/file-sessions/${fileSessionId}/write-batch`, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ writes: [{ path: "docs/readme.txt", contentBase64: Buffer.from("file-session").toString("base64") }] }),
|
|
});
|
|
const writeBatchBody = await writeBatch.json();
|
|
expect(writeBatch.status).toBe(200);
|
|
|
|
const staleBatch = await app.request(`http://openwork.local/workspaces/${workspaceId}/file-sessions/${fileSessionId}/write-batch`, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({
|
|
writes: [{
|
|
path: "docs/readme.txt",
|
|
contentBase64: Buffer.from("stale").toString("base64"),
|
|
ifMatchRevision: "1:1",
|
|
}],
|
|
}),
|
|
});
|
|
const staleBatchBody = await staleBatch.json();
|
|
expect(staleBatch.status).toBe(200);
|
|
expect(staleBatchBody.data.items[0].code).toBe("conflict");
|
|
expect(staleBatchBody.data.items[0].currentRevision).toBe(writeBatchBody.data.items[0].revision);
|
|
|
|
const readBatch = await app.request(`http://openwork.local/workspaces/${workspaceId}/file-sessions/${fileSessionId}/read-batch`, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ paths: ["docs/readme.txt"] }),
|
|
});
|
|
const readBatchBody = await readBatch.json();
|
|
expect(readBatch.status).toBe(200);
|
|
expect(Buffer.from(readBatchBody.data.items[0].contentBase64, "base64").toString("utf8")).toBe("file-session");
|
|
|
|
const ops = await app.request(`http://openwork.local/workspaces/${workspaceId}/file-sessions/${fileSessionId}/operations`, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ operations: [{ type: "rename", from: "docs/readme.txt", to: "docs/renamed.txt" }] }),
|
|
});
|
|
expect(ops.status).toBe(200);
|
|
|
|
const catalog = await app.request(`http://openwork.local/workspaces/${workspaceId}/file-sessions/${fileSessionId}/catalog/snapshot?prefix=docs`);
|
|
const catalogBody = await catalog.json();
|
|
expect(catalog.status).toBe(200);
|
|
expect(catalogBody.data.items.some((item: any) => item.path === "docs/renamed.txt")).toBe(true);
|
|
|
|
const upload = await app.request(`http://openwork.local/workspaces/${workspaceId}/inbox`, {
|
|
method: "POST",
|
|
body: (() => {
|
|
const form = new FormData();
|
|
form.append("file", new File(["hello inbox"], "hello.txt", { type: "text/plain" }));
|
|
return form;
|
|
})(),
|
|
});
|
|
const uploadBody = await upload.json();
|
|
expect(upload.status).toBe(200);
|
|
expect(uploadBody.data.path).toBe("hello.txt");
|
|
|
|
const inboxList = await app.request(`http://openwork.local/workspaces/${workspaceId}/inbox`);
|
|
const inboxListBody = await inboxList.json();
|
|
expect(inboxList.status).toBe(200);
|
|
expect(inboxListBody.data.items[0].name).toBe("hello.txt");
|
|
|
|
const outboxDir = path.join(workspaceRoot, ".opencode", "openwork", "outbox");
|
|
fs.mkdirSync(outboxDir, { recursive: true });
|
|
fs.writeFileSync(path.join(outboxDir, "artifact.bin"), "artifact", "utf8");
|
|
|
|
const artifacts = await app.request(`http://openwork.local/workspaces/${workspaceId}/artifacts`);
|
|
const artifactsBody = await artifacts.json();
|
|
expect(artifacts.status).toBe(200);
|
|
expect(artifactsBody.data.items[0].path).toBe("artifact.bin");
|
|
|
|
const reloads = await app.request(`http://openwork.local/workspaces/${workspaceId}/reload-events`);
|
|
const reloadBody = await reloads.json();
|
|
expect(reloads.status).toBe(200);
|
|
expect(reloadBody.data.items.length).toBeGreaterThan(0);
|
|
expect(reloadBody.data.items.some((item: any) => item.reason === "config")).toBe(true);
|
|
|
|
const disposed = await app.request(`http://openwork.local/workspaces/${workspaceId}/dispose`, {
|
|
method: "POST",
|
|
});
|
|
const disposedBody = await disposed.json();
|
|
expect(disposed.status).toBe(200);
|
|
expect(disposedBody.data.disposed).toBe(true);
|
|
});
|
|
|
|
test("remote workspace config and file routes proxy through the local server", async () => {
|
|
const remote = Bun.serve({
|
|
fetch(request) {
|
|
const url = new URL(request.url);
|
|
if (url.pathname === "/workspaces/remote-alpha/config" && request.method === "GET") {
|
|
return Response.json({
|
|
ok: true,
|
|
data: {
|
|
effective: { opencode: { permission: { external_directory: { "/srv/alpha/*": "allow" } } }, openwork: {} },
|
|
materialized: { compatibilityOpencodePath: null, compatibilityOpenworkPath: null, configDir: "/srv/config", configOpenworkPath: "/srv/config/.opencode/openwork.json", configOpencodePath: "/srv/config/opencode.jsonc" },
|
|
stored: { openwork: { reload: { auto: true } }, opencode: {} },
|
|
updatedAt: new Date().toISOString(),
|
|
workspaceId: "remote-alpha",
|
|
},
|
|
meta: { requestId: "owreq_remote_cfg_1", timestamp: new Date().toISOString() },
|
|
});
|
|
}
|
|
if (url.pathname === "/workspaces/remote-alpha/config" && request.method === "PATCH") {
|
|
return Response.json({
|
|
ok: true,
|
|
data: {
|
|
effective: { opencode: { permission: { external_directory: { "/srv/alpha/*": "allow", "/srv/shared/*": "allow" } } }, openwork: {} },
|
|
materialized: { compatibilityOpencodePath: null, compatibilityOpenworkPath: null, configDir: "/srv/config", configOpenworkPath: "/srv/config/.opencode/openwork.json", configOpencodePath: "/srv/config/opencode.jsonc" },
|
|
stored: { openwork: { reload: { auto: true } }, opencode: {} },
|
|
updatedAt: new Date().toISOString(),
|
|
workspaceId: "remote-alpha",
|
|
},
|
|
meta: { requestId: "owreq_remote_cfg_2", timestamp: new Date().toISOString() },
|
|
});
|
|
}
|
|
if (url.pathname === "/workspaces/remote-alpha/files/content" && request.method === "GET") {
|
|
return Response.json({ ok: true, data: { path: "notes.md", content: "remote hello", bytes: 12, updatedAt: 42 }, meta: { requestId: "owreq_remote_file_1", timestamp: new Date().toISOString() } });
|
|
}
|
|
if (url.pathname === "/workspaces/remote-alpha/files/content" && request.method === "POST") {
|
|
return Response.json({ ok: true, data: { path: "notes.md", bytes: 12, revision: "42:12", updatedAt: 43 }, meta: { requestId: "owreq_remote_file_2", timestamp: new Date().toISOString() } });
|
|
}
|
|
if (url.pathname === "/workspaces/remote-alpha/reload-events" && request.method === "GET") {
|
|
return Response.json({ ok: true, data: { cursor: 1, items: [{ id: "evt_remote_1", reason: "config", seq: 1, timestamp: Date.now(), workspaceId: "remote-alpha" }] }, meta: { requestId: "owreq_remote_reload_1", timestamp: new Date().toISOString() } });
|
|
}
|
|
return new Response("not found", { status: 404 });
|
|
},
|
|
hostname: "127.0.0.1",
|
|
port: 0,
|
|
});
|
|
|
|
try {
|
|
const { app, dependencies } = createTestApp();
|
|
const workspace = dependencies.persistence.registry.importRemoteWorkspace({
|
|
baseUrl: `http://127.0.0.1:${remote.port}`,
|
|
displayName: "Remote Alpha",
|
|
legacyNotes: {},
|
|
remoteType: "openwork",
|
|
remoteWorkspaceId: "remote-alpha",
|
|
serverAuth: { openworkToken: "remote-token" },
|
|
serverBaseUrl: `http://127.0.0.1:${remote.port}`,
|
|
serverHostingKind: "self_hosted",
|
|
serverLabel: `127.0.0.1:${remote.port}`,
|
|
workspaceStatus: "ready",
|
|
});
|
|
|
|
const config = await app.request(`http://openwork.local/workspaces/${workspace.id}/config`);
|
|
const configBody = await config.json();
|
|
expect(config.status).toBe(200);
|
|
expect(configBody.data.stored.openwork.reload.auto).toBe(true);
|
|
|
|
const patched = await app.request(`http://openwork.local/workspaces/${workspace.id}/config`, {
|
|
method: "PATCH",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ opencode: { permission: { external_directory: { "/srv/shared/*": "allow" } } } }),
|
|
});
|
|
expect(patched.status).toBe(200);
|
|
|
|
const contentRead = await app.request(`http://openwork.local/workspaces/${workspace.id}/files/content?path=notes.md`);
|
|
const contentBody = await contentRead.json();
|
|
expect(contentRead.status).toBe(200);
|
|
expect(contentBody.data.content).toBe("remote hello");
|
|
|
|
const contentWrite = await app.request(`http://openwork.local/workspaces/${workspace.id}/files/content`, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ path: "notes.md", content: "remote hello" }),
|
|
});
|
|
const contentWriteBody = await contentWrite.json();
|
|
expect(contentWrite.status).toBe(200);
|
|
expect(contentWriteBody.data.revision).toBe("42:12");
|
|
|
|
const reloads = await app.request(`http://openwork.local/workspaces/${workspace.id}/reload-events`);
|
|
const reloadBody = await reloads.json();
|
|
expect(reloads.status).toBe(200);
|
|
expect(reloadBody.data.items[0].workspaceId).toBe("remote-alpha");
|
|
} finally {
|
|
remote.stop(true);
|
|
}
|
|
});
|
|
|
|
test("reconciliation absorbs recognized managed items from local workspace files", async () => {
|
|
const { dependencies, root } = createTestApp();
|
|
const workspaceRoot = path.join(root, "workspace-gamma");
|
|
fs.mkdirSync(path.join(workspaceRoot, ".opencode", "skills", "manual-skill"), { recursive: true });
|
|
fs.writeFileSync(path.join(workspaceRoot, "opencode.jsonc"), JSON.stringify({
|
|
$schema: "https://opencode.ai/config.json",
|
|
mcp: {
|
|
demo: { type: "local", command: ["demo"] },
|
|
},
|
|
plugin: ["demo-plugin"],
|
|
provider: {
|
|
openai: { options: { apiKey: "redacted" } },
|
|
},
|
|
}, null, 2), "utf8");
|
|
fs.writeFileSync(path.join(workspaceRoot, ".opencode", "skills", "manual-skill", "SKILL.md"), "---\nname: manual-skill\ndescription: Manual skill\n---\n\nhello\n", "utf8");
|
|
|
|
const workspace = dependencies.persistence.registry.importLocalWorkspace({
|
|
dataDir: workspaceRoot,
|
|
displayName: "Gamma",
|
|
status: "ready",
|
|
});
|
|
|
|
dependencies.services.config.reconcileAllWorkspaces();
|
|
|
|
const mcps = dependencies.persistence.repositories.workspaceMcps.listForWorkspace(workspace.id);
|
|
const plugins = dependencies.persistence.repositories.workspacePlugins.listForWorkspace(workspace.id);
|
|
const providers = dependencies.persistence.repositories.workspaceProviderConfigs.listForWorkspace(workspace.id);
|
|
const skills = dependencies.persistence.repositories.workspaceSkills.listForWorkspace(workspace.id);
|
|
const snapshot = await dependencies.services.config.getWorkspaceConfigSnapshot(workspace.id);
|
|
|
|
expect(mcps).toHaveLength(1);
|
|
expect(plugins).toHaveLength(1);
|
|
expect(providers).toHaveLength(1);
|
|
expect(skills).toHaveLength(1);
|
|
expect(snapshot.stored.opencode.mcp).toBeUndefined();
|
|
expect((snapshot.effective.opencode.mcp as any).demo.type).toBe("local");
|
|
expect(snapshot.effective.opencode.plugin).toContain("demo-plugin");
|
|
expect((snapshot.effective.opencode.provider as any).openai.options.apiKey).toBe("redacted");
|
|
});
|