Review and harden mute-user: kill mutation survivors, refresh manifests

Add hardening tests to kill mutation survivors in MemoMute.validate,
ProfilePage constructor/_loadState/canMute, MuteQuery._nextString,
MuteState validation guards, and the mute REST controller statused-error
path. Refresh mutation manifests and Gherkin acceptance-mutation stamps.

By architect.
This commit is contained in:
Chris Troutner
2026-08-28 21:08:50 -07:00
parent d8ab5bc79c
commit ac468072df
25 changed files with 165 additions and 9 deletions
+4
View File
@@ -1,3 +1,7 @@
# acceptance-mutation-manifest-begin
# {"version":1,"tested_at":"2026-08-29T03:43:35.749097546Z","feature_name":"Mute Read","feature_path":"/home/trout/work/psf-memo/.worktrees/architect/psf-memo-db/specs/mute-read.feature","background_hash":"a5f096996120c0f8eb55576184704c74a5e3487999edec263abee0d8fdd53348","implementation_hash":"unknown","scenarios":[{"index":1,"name":"Mute Read - 2 GET /mute/muted lists the addresses a muter mutes","scenario_hash":"ad2bc8a39c2450f1391aa39d246f30989536f3d94b461302bd9528c029641362","mutation_count":4,"result":{"Total":4,"Killed":4,"Survived":0,"Errors":0},"tested_at":"2026-08-29T03:43:35.749097546Z"}]}
# acceptance-mutation-manifest-end
# Scenarios: Mute Read - 1, Mute Read - 2
#
# The mutes store keys mutees by 20-byte hash160. Use bch-js
+1 -1
View File
@@ -63,5 +63,5 @@ class Adapters {
export default Adapters
// mutate4javascript-manifest-begin
// {"version":1,"tested_at":"2026-08-28T23:02:23.414Z","module_hash":"b21f1e3f06db5dd67606fbc7021677bca4d9db8e900464185e89de2d1c2a61f1","functions":[{"id":"func/Adapters.constructor","name":"Adapters.constructor","line":14,"end_line":18,"hash":"065fc13eb85e8f884084eb672e5bf38175fb8305e576484804398dd902133c40"},{"id":"func/Adapters.openDatabases","name":"Adapters.openDatabases","line":20,"end_line":50,"hash":"6666e105f384adb9885d102b05b1be0f5b9885592c9ff1f8db34843ec351be01"},{"id":"func/Adapters.start","name":"Adapters.start","line":52,"end_line":56,"hash":"f9e62a9199f0259f5c22913625e6e497548b3887cc24871bba475e73e52b3750"}]}
// {"version":1,"tested_at":"2026-08-29T03:34:22.589Z","module_hash":"7137b595886dd88a8e876e38126ab9abe106b5b0e1054092297f508fb757adc4","functions":[{"id":"func/Adapters.constructor","name":"Adapters.constructor","line":15,"end_line":19,"hash":"065fc13eb85e8f884084eb672e5bf38175fb8305e576484804398dd902133c40"},{"id":"func/Adapters.openDatabases","name":"Adapters.openDatabases","line":21,"end_line":54,"hash":"588b692236c37e0940c82f235d15be1a8640b4afa8bc2a0a06ada5026dd417f5"},{"id":"func/Adapters.start","name":"Adapters.start","line":56,"end_line":60,"hash":"f9e62a9199f0259f5c22913625e6e497548b3887cc24871bba475e73e52b3750"}]}
// mutate4javascript-manifest-end
+4
View File
@@ -66,3 +66,7 @@ class MuteQuery {
}
export default MuteQuery
// mutate4javascript-manifest-begin
// {"version":1,"tested_at":"2026-08-29T03:27:18.711Z","module_hash":"ea0307363359511b441e8b8dcdfb98c1020a605eac066df6dbdd8bf483d27cae","functions":[{"id":"func/MuteQuery.constructor","name":"MuteQuery.constructor","line":16,"end_line":25,"hash":"5f3104002d87ee9f837d862cbc64197963704c5fd3aa3631d5c5e71f6cc59f21"},{"id":"func/MuteQuery.isMuted","name":"MuteQuery.isMuted","line":29,"end_line":39,"hash":"76d1d6507cb040a3b3cecd1fee11b140f96d3836a05ee912f45b6534d55a1a50"},{"id":"func/MuteQuery.listMuted","name":"MuteQuery.listMuted","line":42,"end_line":51,"hash":"290ffc75a83bc51226d915b4962beacec7a48d611367e1bc7e4144056b3ae2f9"},{"id":"func/MuteQuery._toHash160","name":"MuteQuery._toHash160","line":53,"end_line":55,"hash":"248ed673be41bc9dcaa0d2fbeed1596784b7bde8877eafe43a05e29e430f9e8b"},{"id":"func/MuteQuery._toCashAddress","name":"MuteQuery._toCashAddress","line":57,"end_line":59,"hash":"0706292919ee44a25e63565558d4d9499188c7c49b3afc39eeb20438f4d5c4d4"},{"id":"func/MuteQuery._nextString","name":"MuteQuery._nextString","line":63,"end_line":65,"hash":"faa7823440ac998c3d5920ebcfe336d7b8df7e705dd6a1177dc8ccbd21f9ecf2"}]}
// mutate4javascript-manifest-end
@@ -77,3 +77,7 @@ class MuteRESTControllerLib {
}
export default MuteRESTControllerLib
// mutate4javascript-manifest-begin
// {"version":1,"tested_at":"2026-08-29T03:32:28.766Z","module_hash":"d64a285141e5c61b633b087777d2649ee4d7597206e59bedc6dbdb797cefddae","functions":[{"id":"func/MuteRESTControllerLib.constructor","name":"MuteRESTControllerLib.constructor","line":8,"end_line":21,"hash":"05eda89865cbf8f59f0d9cafe012c2185eeff9082d90636e6f8675f9b2df8453"},{"id":"func/MuteRESTControllerLib.handleError","name":"MuteRESTControllerLib.handleError","line":23,"end_line":30,"hash":"a2129f52ffeeae961205b6fca5bf2f01e35494e724c5b72ee314013b343112c9"},{"id":"func/MuteRESTControllerLib.getMuteState","name":"MuteRESTControllerLib.getMuteState","line":47,"end_line":54,"hash":"3c140541ac30e5c8f029fc9b6b9c17ec09b096589e78f28b7565079fc50c639d"},{"id":"func/MuteRESTControllerLib.getMuted","name":"MuteRESTControllerLib.getMuted","line":69,"end_line":76,"hash":"aa4555e7b3ea3ec283273dd1ee53f8bf577fc591317cd211609c73f1863c4140"}]}
// mutate4javascript-manifest-end
@@ -33,3 +33,7 @@ class MuteRouter {
}
export default MuteRouter
// mutate4javascript-manifest-begin
// {"version":1,"tested_at":"2026-08-29T03:31:02.844Z","module_hash":"0539f755b7b843d59c73484a1c2411539198ddb243bdda64f35b4cab9d84d62d","functions":[{"id":"func/MuteRouter.constructor","name":"MuteRouter.constructor","line":9,"end_line":24,"hash":"70b571eea7a91fc3c2b2eecff7dc9003d679b2510813999cc63f81559032f094"},{"id":"func/MuteRouter.attach","name":"MuteRouter.attach","line":26,"end_line":32,"hash":"0f54317bf9a10a21fcbf15a722394d483b9089ca22661e0c1a43de32f45fb460"}]}
// mutate4javascript-manifest-end
+1 -1
View File
@@ -121,5 +121,5 @@ class UseCases {
export default UseCases
// mutate4javascript-manifest-begin
// {"version":1,"tested_at":"2026-08-28T23:02:43.390Z","module_hash":"cf2bffa7de787c479a3849641f6f6ead863cea555a6c2db995179732bcf05466","functions":[{"id":"func/UseCases.constructor","name":"UseCases.constructor","line":21,"end_line":44,"hash":"3fb83ec6337d0634fa8b1e7431e0e94c2aa4be2d3cbc930841d294a08b465f98"},{"id":"func/UseCases.start","name":"UseCases.start","line":46,"end_line":106,"hash":"c876c85df5fd28146e8f6bce6aa7c48eed7ff3303cb3384cf0a9704926b837a3"}]}
// {"version":1,"tested_at":"2026-08-29T03:34:22.727Z","module_hash":"bd1af3666615a359eb970d1576472fe7a792fbfd135972e81ba50f1441976e94","functions":[{"id":"func/UseCases.constructor","name":"UseCases.constructor","line":23,"end_line":48,"hash":"511ff53d6fe3084c42bc443e67183991e065fe6432d26291d8f44fcf3ee3fe94"},{"id":"func/UseCases.start","name":"UseCases.start","line":50,"end_line":118,"hash":"4a94f92a0c335ee536da858df44d13e81930b86df9e27ebff75d71753865e299"}]}
// mutate4javascript-manifest-end
@@ -30,5 +30,5 @@ export class FollowListUseCase extends ListUseCase {
}
// mutate4javascript-manifest-begin
// {"version":1,"tested_at":"2026-08-27T18:16:27.234Z","module_hash":"1424c15ec8acd2179412ef5b347107746ef1e035a47fc1fac595e96c194526f8","functions":[{"id":"func/FollowListUseCase.constructor","name":"FollowListUseCase.constructor","line":15,"end_line":20,"hash":"f5f3b2832eb47baeb416b6b36122163d7479444e82f471a7d9b34f800639208c"},{"id":"func/FollowListUseCase.execute","name":"FollowListUseCase.execute","line":22,"end_line":29,"hash":"3d9258e7bb03192db5b56eac38819604dfdc43ee63b47cc6445754d36ab1848f"}]}
// {"version":1,"tested_at":"2026-08-29T03:30:18.194Z","module_hash":"24024ec5900b4097a6aa7aef2e5d4eb92834336ee99a7143ef59e3565b112029","functions":[{"id":"func/FollowListUseCase.constructor","name":"FollowListUseCase.constructor","line":15,"end_line":20,"hash":"3959516f05104386f09b2194feda32dd46038975dff2cfedf58b82da0bf08d10"},{"id":"func/FollowListUseCase.execute","name":"FollowListUseCase.execute","line":22,"end_line":29,"hash":"3d9258e7bb03192db5b56eac38819604dfdc43ee63b47cc6445754d36ab1848f"}]}
// mutate4javascript-manifest-end
+4
View File
@@ -19,3 +19,7 @@ class ListMuted extends FollowListUseCase {
}
export default ListMuted
// mutate4javascript-manifest-begin
// {"version":1,"tested_at":"2026-08-29T03:29:41.578Z","module_hash":"3fd8a3c1a800e95a07d8fe9d992b7004da225563ab47d76308aad93b2ee77efd","functions":[{"id":"func/ListMuted.constructor","name":"ListMuted.constructor","line":10,"end_line":18,"hash":"2c77d678a9c501f0a0c33f73155415928eeb0738247da09ebd5ad5cbf8003b84"}]}
// mutate4javascript-manifest-end
+4
View File
@@ -27,3 +27,7 @@ class MuteState extends ListUseCase {
}
export default MuteState
// mutate4javascript-manifest-begin
// {"version":1,"tested_at":"2026-08-29T03:29:04.639Z","module_hash":"0e37ef99c98cac6f23c11f10e4d9624ce6592d5842aa27b4ddebd69e0510a942","functions":[{"id":"func/MuteState.constructor","name":"MuteState.constructor","line":10,"end_line":12,"hash":"bf68be0e0fe9b7940cdcd450edd9e41a27e9bcc42f69a1d691461fd3f5ba5c9a"},{"id":"func/MuteState.execute","name":"MuteState.execute","line":14,"end_line":26,"hash":"0ff800d8e1f5c3a0fd25aa51aff6346eed3928739da7643071c12e4c02212399"}]}
// mutate4javascript-manifest-end
@@ -75,6 +75,14 @@ describe('#MuteQuery', () => {
assert.equal(result, false)
})
it('_nextString returns the lexicographic exclusive upper bound of a prefix', () => {
const query = new MuteQuery({ mutesDb: makeMutesDb({}) })
// 'abc:' -> drop the trailing ':', increment the last char (':' -> ';').
assert.equal(query._nextString('abc:'), 'abc;')
assert.equal(query._nextString('a'), 'b')
})
it('listMuted returns active mutees for a muter', async () => {
const hash160 = 'cb481232299cd5743151ac4b2d63ae198e7bb0a9'
const hash160Two = '44c44cfcb6e4e00386c7b0d14eaac6b7f47695e3'
@@ -60,4 +60,32 @@ describe('#MuteRESTController', () => {
assert.equal(ctx.throw.callCount, 1)
assert.equal(ctx.throw.firstCall.args[0], 500)
})
it('should rethrow a statused error with its message', async () => {
const err = new Error('bad request')
err.status = 400
const muteState = { execute: sandbox.stub().rejects(err) }
const uut = makeUut({ muteState })
const ctx = makeCtx({ muter: 'a', mutee: 'b' })
await uut.getMuteState(ctx)
assert.equal(ctx.throw.callCount, 1)
assert.equal(ctx.throw.firstCall.args[0], 400)
assert.equal(ctx.throw.firstCall.args[1], 'bad request')
})
it('should rethrow a statused error without a message', async () => {
const err = new Error()
err.status = 404
const muteState = { execute: sandbox.stub().rejects(err) }
const uut = makeUut({ muteState })
const ctx = makeCtx({ muter: 'a', mutee: 'b' })
await uut.getMuteState(ctx)
assert.equal(ctx.throw.callCount, 1)
assert.equal(ctx.throw.firstCall.args[0], 404)
assert.equal(ctx.throw.firstCall.args[1], err)
})
})
@@ -69,4 +69,44 @@ describe('#MuteState', () => {
assert.match(err.message, /muteeAddr is required/)
}
})
it('should reject an empty-string muterAddr', async () => {
const useCase = new MuteState({ adapters: makeAdapters(false) })
try {
await useCase.execute({ muterAddr: '', muteeAddr: 'bitcoincash:mutee' })
assert.fail('expected error')
} catch (err) {
assert.match(err.message, /muterAddr is required/)
}
})
it('should reject a non-string muterAddr', async () => {
const useCase = new MuteState({ adapters: makeAdapters(false) })
try {
await useCase.execute({ muterAddr: 123, muteeAddr: 'bitcoincash:mutee' })
assert.fail('expected error')
} catch (err) {
assert.match(err.message, /muterAddr is required/)
}
})
it('should reject an empty-string muteeAddr', async () => {
const useCase = new MuteState({ adapters: makeAdapters(false) })
try {
await useCase.execute({ muterAddr: 'bitcoincash:muter', muteeAddr: '' })
assert.fail('expected error')
} catch (err) {
assert.match(err.message, /muteeAddr is required/)
}
})
it('should reject a non-string muteeAddr', async () => {
const useCase = new MuteState({ adapters: makeAdapters(false) })
try {
await useCase.execute({ muterAddr: 'bitcoincash:muter', muteeAddr: 456 })
assert.fail('expected error')
} catch (err) {
assert.match(err.message, /muteeAddr is required/)
}
})
})