fix(api): store LF line breaks for multipart image notes; make ACP host tests portable (#568)
* fix(api): store LF line breaks for multipart image notes multipart/form-data encodes line breaks in text fields as CRLF. Browsers always do so, and Bun 1.4 now does as well, so notes created through POST /api/v1/memos/with-image (used by the Web Clipper) were stored with CRLF line breaks, while every other note uses LF. The route now normalizes the Markdown field to LF before creating the note. This also fixes the existing route contract test under Bun 1.4, and a new test sends a CRLF multipart body like a browser does. * test(desktop): make ACP host tests independent of the machine and Bun version - The ACP command allow-list test checked that a missing command is not installed, but on macOS the resolver also looks in /opt/homebrew/bin and /usr/local/bin, so it failed on any machine with that command installed. The test now only sees its temporary directory. - The fake WorkBuddy agent started its account API without a port during probes and chats. Bun 1.3 then listened on a random port, while Bun 1.4, like Node, rejects port NaN, so six WorkBuddy session tests hung until their timeout. Like WorkBuddy itself, the fake agent now serves the account API only when started with a port.
This commit is contained in:
1 parent
9bb3218c03
commit
bbe80d18db
3 files changed
+44
-3
No files matched your search
@@ -158,6 +158,34 @@ describe("memo route contracts", () => {
|
||||
expect(await response.json()).toMatchObject({ memo: { id: "memo_created" } });
|
||||
});
|
||||
|
||||
test("stores the line breaks of a multipart image note as LF", async () => {
|
||||
let contentMarkdown;
|
||||
const dependencies = createDependencies({
|
||||
createMemo: async (_db, _workspaceId, input) => {
|
||||
contentMarkdown = input.contentMarkdown;
|
||||
return { id: "memo_created" };
|
||||
},
|
||||
});
|
||||
// Browsers encode line breaks in multipart text fields as CRLF.
|
||||
const boundary = "edgeever-boundary";
|
||||
const part = (name, value) => `--${boundary}\r\nContent-Disposition: form-data; name="${name}"\r\n\r\n${value}\r\n`;
|
||||
const body = [
|
||||
part("notebookId", "nb_1"),
|
||||
part("contentMarkdown", "\r\n\r\nSource: https://example.com\r\nLine"),
|
||||
`--${boundary}\r\nContent-Disposition: form-data; name="file"; filename="clip.png"\r\nContent-Type: image/png\r\n\r\n\x89PNG\r\n`,
|
||||
`--${boundary}--\r\n`,
|
||||
].join("");
|
||||
const response = await createApp(dependencies).request("/api/v1/memos/with-image", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": `multipart/form-data; boundary=${boundary}` },
|
||||
body,
|
||||
}, environment);
|
||||
|
||||
expect(response.status).toBe(201);
|
||||
const { resourceId } = await response.json();
|
||||
expect(contentMarkdown).toBe(`\n\nSource: https://example.com\nLine`);
|
||||
});
|
||||
|
||||
test("enforces read scope before loading a memo", async () => {
|
||||
let called = false;
|
||||
const dependencies = createDependencies({
|
||||
|
||||
@@ -172,7 +172,11 @@ export const registerMemoRoutes = (
|
||||
const file = form.get("file");
|
||||
const notebookId = typeof form.get("notebookId") === "string" ? String(form.get("notebookId")).trim() : "";
|
||||
const title = typeof form.get("title") === "string" ? String(form.get("title")) : "";
|
||||
const contentMarkdown = typeof form.get("contentMarkdown") === "string" ? String(form.get("contentMarkdown")) : "";
|
||||
// multipart/form-data encodes line breaks in text fields as CRLF (browsers
|
||||
// always, and newer Bun versions too), while notes store LF line breaks.
|
||||
const contentMarkdown = typeof form.get("contentMarkdown") === "string"
|
||||
? String(form.get("contentMarkdown")).replace(/\r\n?/g, "\n")
|
||||
: "";
|
||||
let tags: string[] = [];
|
||||
const tagsValue = form.get("tags");
|
||||
if (typeof tagsValue === "string" && tagsValue.trim()) {
|
||||
|
||||
@@ -104,7 +104,10 @@ if (${workBuddyAccountApi ? "true" : "false"}) {
|
||||
if (${workBuddyUnreadable ? "true" : "false"}) authenticated = true;
|
||||
const restoredAccount = authenticated;
|
||||
const save = () => writeFileSync(reportPath, JSON.stringify(report));
|
||||
if (${workBuddyAccountApi ? "true" : "false"}) {
|
||||
// Like WorkBuddy, serve the account API only when started with --serve, which
|
||||
// sets SERVER__PORT. Probes and chats start without it, and listening on port
|
||||
// NaN is an error in Node and newer Bun versions.
|
||||
if (${workBuddyAccountApi ? "true" : "false"} && process.env.SERVER__PORT) {
|
||||
const { createServer } = await import("node:http");
|
||||
const server = createServer(async (req, res) => {
|
||||
if (req.headers.authorization !== "Bearer " + process.env.CODEBUDDY_GATEWAY_PASSWORD) {
|
||||
@@ -348,7 +351,13 @@ describe("ACP command allow-list", () => {
|
||||
for (const name of ["claude-agent-acp", "openclaw", "hermes"]) {
|
||||
await writeFile(path.join(directory, name), "#!/bin/sh\nexit 0\n", { mode: 0o755 });
|
||||
}
|
||||
const deps = { platform: "darwin", pathEnv: directory, home: directory };
|
||||
// Only the temporary directory exists, so commands installed on the
|
||||
// machine running the tests (for example in /opt/homebrew/bin) are ignored.
|
||||
const onlyTemporaryDirectory = (candidate) => {
|
||||
if (!candidate.startsWith(directory)) throw Object.assign(new Error(`ENOENT: ${candidate}`), { code: "ENOENT" });
|
||||
return realpathSync(candidate);
|
||||
};
|
||||
const deps = { platform: "darwin", pathEnv: directory, home: directory, realpathSync: onlyTemporaryDirectory };
|
||||
expect(resolveAcpCommand({ id: "claudeCode" }, deps).command).toMatchObject({ command: realpathSync(path.join(directory, "claude-agent-acp")), args: [] });
|
||||
expect(resolveAcpCommand({ id: "openClaw" }, deps).command).toMatchObject({ command: realpathSync(path.join(directory, "openclaw")), args: ["acp"] });
|
||||
expect(resolveAcpCommand({ id: "hermesAgent" }, deps).command).toMatchObject({ command: realpathSync(path.join(directory, "hermes")), args: ["acp"] });
|
||||
|
||||
Reference in new issue
Block a user