Revert "fix(api3): dedup AAPS profile-store edits via REST POST /v3/profile"

This reverts commit 3b786ab3.

On review the V3 (app, defaultProfile) collapse was too aggressive and
broke parity with how the rest of the ecosystem treats the profile
collection:

- Loop (NightscoutKit) and Trio (NightscoutAPI.swift:411) both POST
  /api/v1/profile without _id on every settings edit, accumulating one
  doc per upload via lib/server/profile.js:create(). They have done so
  for years.
- The Nightscout profile collection is historical/append-only by
  design; the NS UI profile editor lets users navigate prior
  snapshots, and lib/server/profile.js:last() picks the most recent
  for display.
- Collapsing AAPS V3 edits onto a single (app, defaultProfile) row
  diverged from Loop/Trio/AAPS-V1 behavior and erased the upload
  history that NS UI exposes.

The original 'AAPS edits not appearing' user complaint is sufficiently
addressed by:
  - the V1 websocket retry dedup (commit 85f7e6ac), which kills the
    60s ack-window race; and
  - the {startDate: -1, _id: -1} secondary sort in profile.last()
    (also 85f7e6ac), which deterministically picks the newest row
    when startDate ties.

Both of those help every uploader (Loop, Trio, AAPS V1, AAPS V3)
without changing the ecosystem-wide profile-as-history semantic. The
characterization tests added in ddabdc6c are restored by this revert
and continue to document V3's request-level (date-based) dedup.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Ben West
2026-04-20 17:49:43 -07:00
co-authored by Copilot
parent 3b786ab3c0
commit c927be7a0d
3 changed files with 8 additions and 60 deletions
+8 -37
View File
@@ -652,11 +652,8 @@ describe('API3 AAPS Patterns - Deduplication and Real-world Scenarios', function
docs.length.should.equal(1);
});
it('AAPS edit (new date from LocalProfileLastChange) DEDUPS onto existing profile (post-fix)', async () => {
// Simulates: user edits profile in AAPS twice -> two distinct LocalProfileLastChange values.
// Pre-fix: V3 inserted a new doc per edit because identifier = uuidv5("undefined_<date>").
// Post-fix: profile-store identifier is uuidv5("profilestore_<app>_<defaultProfile>"),
// so edits with the same (app, defaultProfile) replace the existing doc.
it('AAPS edit (new date from LocalProfileLastChange) creates a SECOND doc, not an update', async () => {
// Simulates: user edits profile in AAPS twice -> two distinct LocalProfileLastChange values
const t1 = Date.now() - 60000;
const first = aapsV3Profile(t1);
first.store['aaps-v3-test'].carbratio[0].value = 8;
@@ -667,16 +664,15 @@ describe('API3 AAPS Patterns - Deduplication and Real-world Scenarios', function
res1.status.should.equal(201);
self.cache.clear();
const res2 = await self.instance.post(url, self.jwt.update).send(second);
// Post-fix: edit dedups in place -> 200, same identifier
res2.status.should.equal(200);
res2.body.identifier.should.equal(res1.body.identifier);
const res2 = await self.instance.post(url, self.jwt.create).send(second);
// V3 inserts a NEW doc because identifier (uuidv5 of "undefined_<date>") differs
res2.status.should.equal(201);
res2.body.identifier.should.not.equal(res1.body.identifier);
const docs = await profileCollection().find({ defaultProfile: 'aaps-v3-test' }).toArray();
docs.length.should.equal(1);
docs[0].store['aaps-v3-test'].carbratio[0].value.should.equal(14);
docs.length.should.equal(2);
// Verify ctx.profile.last() returns the updated profile (post-fix sort: startDate desc, _id desc)
// Verify ctx.profile.last() returns the newer profile (post-fix sort: startDate desc, _id desc)
await new Promise((resolve, reject) => {
self.instance.ctx.profile.last((err, lastDocs) => {
if (err) return reject(err);
@@ -688,30 +684,5 @@ describe('API3 AAPS Patterns - Deduplication and Real-world Scenarios', function
});
});
});
it('different defaultProfile names produce distinct V3 profile docs', async () => {
// Two different profile names should NOT collide under the new dedup key.
const a = aapsV3Profile(Date.now());
a.defaultProfile = 'aaps-v3-test';
a.store = { 'aaps-v3-test': a.store['aaps-v3-test'] };
const b = aapsV3Profile(Date.now() + 1);
b.defaultProfile = 'aaps-v3-test-other';
b.store = { 'aaps-v3-test-other': a.store['aaps-v3-test'] };
const resA = await self.instance.post(url, self.jwt.create).send(a);
resA.status.should.equal(201);
self.cache.clear();
const resB = await self.instance.post(url, self.jwt.create).send(b);
resB.status.should.equal(201);
resB.body.identifier.should.not.equal(resA.body.identifier);
const docsA = await profileCollection().find({ defaultProfile: 'aaps-v3-test' }).toArray();
const docsB = await profileCollection().find({ defaultProfile: 'aaps-v3-test-other' }).toArray();
docsA.length.should.equal(1);
docsB.length.should.equal(1);
// cleanup the extra one
await profileCollection().deleteMany({ defaultProfile: 'aaps-v3-test-other' });
});
});
});