fix: appendText API now attributes text to the specified author (#7446)

* fix: appendText API now attributes text to the specified author

spliceText() was calling makeSplice() without passing author attributes,
so inserted text had no authorship attribution in the changeset — even
though the authorId was recorded in the revision metadata. Now passes
[['author', authorId]] and the pool to makeSplice() so the changeset
ops carry the author attribute, making the text show the author's color
in the editor and appear in listAuthorsOfPad.

Also fixed the same issue in pad init (first changeset creation) and
updated PadType interface to include the authorId parameter.

Fixes #6873

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* test: assert API response code on createPad and anonymous appendText

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
John McLear 2026-04-04 09:28:09 +01:00 committed by GitHub
parent 928eef8978
commit f7e4100aba
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 101 additions and 3 deletions

View File

@ -294,7 +294,8 @@ class Pad {
(!ins && start > 0 && orig[start - 1] === '\n');
if (!willEndWithNewline) ins += '\n';
if (ndel === 0 && ins.length === 0) return;
const changeset = makeSplice(orig, start, ndel, ins);
const attribs = authorId ? [['author', authorId] as [string, string]] : undefined;
const changeset = makeSplice(orig, start, ndel, ins, attribs, this.pool);
await this.appendRevision(changeset, authorId);
}
@ -394,7 +395,8 @@ class Pad {
if (context.type !== 'text') throw new Error(`unsupported content type: ${context.type}`);
text = exports.cleanText(context.content);
}
const firstChangeset = makeSplice('\n', 0, 0, text);
const firstAttribs = authorId ? [['author', authorId] as [string, string]] : undefined;
const firstChangeset = makeSplice('\n', 0, 0, text, firstAttribs, this.pool);
await this.appendRevision(firstChangeset, authorId);
}
await hooks.aCallAll('padLoad', {pad: this});

View File

@ -15,7 +15,7 @@ export type PadType = {
remove: ()=>Promise<void>,
text: ()=>string,
setText: (text: string, authorId?: string)=>Promise<void>,
appendText: (text: string)=>Promise<void>,
appendText: (text: string, authorId?: string)=>Promise<void>,
getHeadRevisionNumber: ()=>number,
getRevisionDate: (rev: number)=>Promise<number>,
getRevisionChangeset: (rev: number)=>Promise<AChangeSet>,

View File

@ -0,0 +1,96 @@
'use strict';
const assert = require('assert').strict;
const common = require('../../common');
let agent: any;
let apiVersion = 1;
const testPadId = `appendTextAuthor_${makeid()}`;
const endPoint = (point: string) => `/api/${apiVersion}/${point}`;
function makeid() {
let text = '';
const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
for (let i = 0; i < 10; i++) {
text += possible.charAt(Math.floor(Math.random() * possible.length));
}
return text;
}
describe(__filename, function () {
let authorId: string;
before(async function () {
agent = await common.init();
const res = await agent.get('/api/')
.expect(200)
.expect('Content-Type', /json/);
apiVersion = res.body.currentVersion;
assert(apiVersion);
// Create an author
const authorRes = await agent.get(`${endPoint('createAuthor')}?name=TestAuthor`)
.set('Authorization', await common.generateJWTToken())
.expect(200);
assert.equal(authorRes.body.code, 0);
authorId = authorRes.body.data.authorID;
assert(authorId);
// Create a pad
await agent.get(`${endPoint('createPad')}?padID=${testPadId}`)
.set('Authorization', await common.generateJWTToken())
.expect(200);
});
it('appendText with authorId attributes the text to that author', async function () {
// Append text with an authorId
const res = await agent.post(endPoint('appendText'))
.set('Authorization', await common.generateJWTToken())
.send({padID: testPadId, text: 'authored text', authorId})
.expect(200)
.expect('Content-Type', /json/);
assert.equal(res.body.code, 0);
// Verify the author appears in the pad's author list
const authorsRes = await agent.get(
`${endPoint('listAuthorsOfPad')}?padID=${testPadId}`)
.set('Authorization', await common.generateJWTToken())
.expect(200);
assert.equal(authorsRes.body.code, 0);
assert(authorsRes.body.data.authorIDs.includes(authorId),
`Expected authorId ${authorId} in pad authors: ${authorsRes.body.data.authorIDs}`);
});
it('appendText without authorId does not attribute to any author', async function () {
const newPadId = `appendTextNoAuthor_${makeid()}`;
const createRes = await agent.get(`${endPoint('createPad')}?padID=${newPadId}`)
.set('Authorization', await common.generateJWTToken())
.expect(200);
assert.equal(createRes.body.code, 0);
const appendRes = await agent.post(endPoint('appendText'))
.set('Authorization', await common.generateJWTToken())
.send({padID: newPadId, text: 'anonymous text'})
.expect(200);
assert.equal(appendRes.body.code, 0);
const authorsRes = await agent.get(
`${endPoint('listAuthorsOfPad')}?padID=${newPadId}`)
.set('Authorization', await common.generateJWTToken())
.expect(200);
assert.equal(authorsRes.body.code, 0);
// No authors should be listed for anonymous text
assert.equal(authorsRes.body.data.authorIDs.length, 0);
await agent.get(`${endPoint('deletePad')}?padID=${newPadId}`)
.set('Authorization', await common.generateJWTToken())
.expect(200);
});
after(async function () {
await agent.get(`${endPoint('deletePad')}?padID=${testPadId}`)
.set('Authorization', await common.generateJWTToken())
.expect(200);
});
});