Send a lone CR through the newline transform, and say what pinned is
`toClaudePayload` matched `/\r?\n/`, so a bare CR that is not part of a CRLF went through verbatim — and a bare CR *submits* in a Claude prompt and *runs* the line in a shell, which is the terminator the function's own contract says it never appends. A `<textarea>` cannot produce one, but `load_in` returns whatever a hand-edited or externally written notes file holds, so the guarantee has to cover that rather than only what the editor can type. `Note.pinned` is persisted and sorted on, but nothing in the app sets it: there is no pin control and no indicator. The spec stated the ordering rule as though pinning existed and §8 did not list it, so the spec is amended to say `pinned` is reserved and inert in v1, and pinning is added to the out-of-scope list. No UI is added — a user-facing affordance does not belong in a fix wave. Also renamed NotesPanel.test.tsx's "deletes the selected note and falls back to another": `useNotes` is mocked in that file and the mocked list never changes, so the fallback was never exercised. A name that claims coverage which is absent is worse than an absent test, because it makes the gap invisible. The real assertion now lives against the real hook in NotesPanel.shared.test.tsx. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HjL1E2JFNctUqCYotUwqqb
This commit is contained in:
@@ -99,7 +99,11 @@ describe("NotesPanel", () => {
|
||||
expect(screen.getByTestId("send")).toHaveTextContent("send:fresh");
|
||||
});
|
||||
|
||||
it("deletes the selected note and falls back to another", async () => {
|
||||
it("asks the hook to delete the selected note", async () => {
|
||||
// Only the call: `useNotes` is mocked here and the mocked list never
|
||||
// changes, so nothing in this file can exercise what the panel selects
|
||||
// afterwards. The fallback is covered against the real hook in
|
||||
// NotesPanel.shared.test.tsx.
|
||||
notes = [note(), note({ id: "n2", title: "Gotchas" })];
|
||||
render(<NotesPanel projectId="p1" />);
|
||||
fireEvent.click(screen.getByRole("button", { name: /delete note/i }));
|
||||
|
||||
@@ -16,6 +16,21 @@ describe("toClaudePayload", () => {
|
||||
expect(toClaudePayload("one\r\ntwo")).toBe("one\x1b\rtwo");
|
||||
});
|
||||
|
||||
it("normalises a lone CR, which would otherwise submit", () => {
|
||||
// A bare \r is a carriage return: it submits in a Claude prompt and runs
|
||||
// the line in a shell — the terminator this function promises not to
|
||||
// append. A textarea cannot make one, but a notes file that was
|
||||
// hand-edited or written by something else can, and `load_in` hands it
|
||||
// straight back.
|
||||
expect(toClaudePayload("one\rtwo")).toBe("one\x1b\rtwo");
|
||||
expect(toClaudePayload("one\rtwo\r\nthree\nfour")).toBe(
|
||||
"one\x1b\rtwo\x1b\rthree\x1b\rfour",
|
||||
);
|
||||
expect(toClaudePayload("text\r").endsWith("\r")).toBe(true);
|
||||
// …but only as the tail of the soft-newline sequence, never bare.
|
||||
expect(toClaudePayload("text\r")).toBe("text\x1b\r");
|
||||
});
|
||||
|
||||
it("leaves single-line text untouched", () => {
|
||||
expect(toClaudePayload("just one line")).toBe("just one line");
|
||||
});
|
||||
|
||||
@@ -23,7 +23,14 @@ export const CLAUDE_SOFT_NEWLINE = "\x1b\r";
|
||||
* as N truncated prompts. Deliberately appends no terminator: the text lands
|
||||
* in the prompt and the user presses Enter, which is what speech-to-text does
|
||||
* for the same reason — an unsent prompt is recoverable and a sent one is not.
|
||||
*
|
||||
* A **lone** `\r` is matched too, not only the one in a CRLF. It is a carriage
|
||||
* return: it submits in a Claude prompt and runs the line in a shell, which is
|
||||
* exactly the terminator this function promises never to append. A `<textarea>`
|
||||
* cannot produce one, but a note body is read back from a JSON file that can be
|
||||
* hand-edited or written by something else, so the guarantee has to hold for
|
||||
* whatever `load_in` returns rather than for whatever the editor can type.
|
||||
*/
|
||||
export function toClaudePayload(text: string): string {
|
||||
return text.replace(/\r?\n/g, CLAUDE_SOFT_NEWLINE);
|
||||
return text.replace(/\r\n|\r|\n/g, CLAUDE_SOFT_NEWLINE);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user