Review and harden search: kill mutation survivors, refresh manifests

Add hardening tests to kill mutation survivors in SearchQuery guards and
profileMatches defaults, SearchAll pagination/empty-query guards, the search
REST controller 500-error path, and the client SearchPage constructor/getPost/
getProfile. Refresh mutation manifests and the Gherkin acceptance-mutation
stamp.

By architect.
This commit is contained in:
Chris Troutner
2026-08-29 08:51:42 -07:00
parent d3e3b23bbe
commit e8bf42a1f3
12 changed files with 193 additions and 0 deletions
@@ -15,3 +15,7 @@ export async function loadReplyTxids (postParentsDb) {
return replyTxids
}
// mutate4javascript-manifest-begin
// {"version":1,"tested_at":"2026-08-29T15:18:40.417Z","module_hash":"4024492a889a87fa3669b0daf955e2a906c75407b2b1f83008458710f1206c9e","functions":[{"id":"func/loadReplyTxids","name":"loadReplyTxids","line":9,"end_line":17,"hash":"17ac31205b8d5e508bda758939bcde282f79add47236c27de07402654ae0b2df"}]}
// mutate4javascript-manifest-end
+4
View File
@@ -136,3 +136,7 @@ class SearchQuery {
}
export default SearchQuery
// mutate4javascript-manifest-begin
// {"version":1,"tested_at":"2026-08-29T15:24:14.179Z","module_hash":"0c6f365ce684e05ae34df5de2fcacaad023d8578a9e228c91a6d3b799f84c3c8","functions":[{"id":"func/SearchQuery.constructor","name":"SearchQuery.constructor","line":14,"end_line":36,"hash":"dc2625c23df7e50c35810379846377d4af23ea56470d7c9308631a74c415cb51"},{"id":"func/SearchQuery.searchPosts","name":"SearchQuery.searchPosts","line":38,"end_line":58,"hash":"d49a70791e72e76f82842226b1a314e803ca5fce772d78beeb73a99069a537ae"},{"id":"func/SearchQuery.isMatchingPost","name":"SearchQuery.isMatchingPost","line":60,"end_line":64,"hash":"1148a189ef5574cb4789a4b12889269212c4bbf97202bccd2fd817d9d40b51a0"},{"id":"func/SearchQuery.searchProfiles","name":"SearchQuery.searchProfiles","line":66,"end_line":75,"hash":"0570020fc12640e30918accca2ea28157fb9a686183f1e3855d209e840fce82a"},{"id":"func/SearchQuery.loadProfileRecords","name":"SearchQuery.loadProfileRecords","line":77,"end_line":84,"hash":"fb67ceac97d5f08e47eca5c136f0b4afe099c4f3425fd83c72e521f9b1871a27"},{"id":"func/SearchQuery.loadRecords","name":"SearchQuery.loadRecords","line":86,"end_line":93,"hash":"2382749d3be862a2b7713550e680f61d7ef64355632f9f3f0fbeeccbf947e13c"},{"id":"func/SearchQuery.matchByName","name":"SearchQuery.matchByName","line":95,"end_line":104,"hash":"5e6e821883e767ec1b3a2ff03d6e19b032d0e4b3d491b59ffe9ab7924abd8edf"},{"id":"func/SearchQuery.matchByText","name":"SearchQuery.matchByText","line":106,"end_line":112,"hash":"4889f4aba2e7c8951b6899a93b2ebefdea6a8b9ee74f4ef2c112c08e3458459c"},{"id":"func/SearchQuery.textMatches","name":"SearchQuery.textMatches","line":114,"end_line":116,"hash":"3db2504b793c5f6648537f1d63969cbe496b9f42c31d4d0060dd527417561129"},{"id":"func/SearchQuery.profileMatches","name":"SearchQuery.profileMatches","line":118,"end_line":135,"hash":"03af6e62f654560d8361ea0e3f508c53e8e7024e65132974c7202b69461441c6"}]}
// mutate4javascript-manifest-end
@@ -60,3 +60,7 @@ class SearchRESTControllerLib {
}
export default SearchRESTControllerLib
// mutate4javascript-manifest-begin
// {"version":1,"tested_at":"2026-08-29T15:29:31.872Z","module_hash":"754b83cf1a4999e989084ba332d88924472370a14df989910d85bcea04deb001","functions":[{"id":"func/SearchRESTControllerLib.constructor","name":"SearchRESTControllerLib.constructor","line":8,"end_line":20,"hash":"75f8c48a39c4449f5b8c8a455d1bc6cdcd762fb035018f75b5e808f093c70c7b"},{"id":"func/SearchRESTControllerLib.handleError","name":"SearchRESTControllerLib.handleError","line":22,"end_line":29,"hash":"d8a071b5d797e24bacd19f63ca74b8eda35dc8d1f4bed97b9e5d37af7bee6831"},{"id":"func/SearchRESTControllerLib.search","name":"SearchRESTControllerLib.search","line":52,"end_line":59,"hash":"814f10f06f8406a61e59aa3678794414a41e8ddecd9693f5c3fa92242349722e"}]}
// mutate4javascript-manifest-end
@@ -39,3 +39,7 @@ class SearchRouter {
}
export default SearchRouter
// mutate4javascript-manifest-begin
// {"version":1,"tested_at":"2026-08-29T15:20:33.643Z","module_hash":"a32dfbdaa55df013a2d49f84a3089f66f38021248e12efcb11407118c94c9bc6","functions":[{"id":"func/SearchRouter.constructor","name":"SearchRouter.constructor","line":9,"end_line":16,"hash":"1eb5af36a47b22f3a868e5b9919dd1014e3a02ad9433f8eb30dda1ea30fdc046"},{"id":"func/SearchRouter.attach","name":"SearchRouter.attach","line":18,"end_line":32,"hash":"5b86e45767e913c1917b019cc2a3a46a150e9830022064012d72c0d6157f0591"},{"id":"func/SearchRouter.search","name":"SearchRouter.search","line":34,"end_line":38,"hash":"5dc63a71388481ce121a1eb449311e6bf09f98ebded6ed8dd5ea62922f2297c0"}]}
// mutate4javascript-manifest-end
+4
View File
@@ -17,3 +17,7 @@ export function sortByHeightDesc (a, b) {
}
return (b.seen || 0) - (a.seen || 0)
}
// mutate4javascript-manifest-begin
// {"version":1,"tested_at":"2026-08-29T15:18:40.416Z","module_hash":"de3b455b6e4376f7a860f74318a42fbe21734f8be5cbcd1a2c261d7c67d1910d","functions":[{"id":"func/normalizeQuery","name":"normalizeQuery","line":10,"end_line":12,"hash":"8257496a9daf753f6928fc7ae709f319231a86dbbd4058727346e6be8cd12896"},{"id":"func/sortByHeightDesc","name":"sortByHeightDesc","line":14,"end_line":19,"hash":"6c1f80c4b6362b75769a45106c24df2dabfd885dec67acd6d3226729905d7c43"}]}
// mutate4javascript-manifest-end
+4
View File
@@ -67,3 +67,7 @@ class SearchAll extends ListUseCase {
}
export default SearchAll
// mutate4javascript-manifest-begin
// {"version":1,"tested_at":"2026-08-29T15:24:14.116Z","module_hash":"c6145beec460f82393ed11ff0d01b72696f557e0e40c2b991ca15e45334cf150","functions":[{"id":"func/SearchAll.constructor","name":"SearchAll.constructor","line":14,"end_line":16,"hash":"adc0dcca1ed5263c0f1e13e8a19657e332982ca6e4f2093080a6a970901ed4d3"},{"id":"func/SearchAll.execute","name":"SearchAll.execute","line":18,"end_line":53,"hash":"ade6f878cca2e20b6e8500cbc6f5650de797767520b31c0d1c29c43762e4ad53"},{"id":"func/SearchAll.emptyResult","name":"SearchAll.emptyResult","line":55,"end_line":66,"hash":"518a3c1be0814ec39ef86770db6f462dfb52bbb622e72b913cb9313cf3b7cc33"}]}
// mutate4javascript-manifest-end
@@ -146,4 +146,87 @@ describe('#SearchQuery', () => {
assert.equal(result.length, 0)
})
it('should return a match for a single-character query', async () => {
postsDb.iterator = () => makeIterator([
['tx1', { addr: 'addr1', text: 'hello world', seen: 100, blockHeight: 600100 }]
])()
postParentsDb.iterator = () => makeIterator([])()
const uut = new SearchQuery({ postsDb, postParentsDb, namesDb, profilesDb })
const result = await uut.searchPosts('h')
assert.equal(result.length, 1)
assert.equal(result[0].txid, 'tx1')
})
it('should skip a null post without throwing', async () => {
postsDb.iterator = () => makeIterator([
['tx1', null],
['tx2', { addr: 'addr2', text: 'hello world', seen: 100, blockHeight: 600100 }]
])()
postParentsDb.iterator = () => makeIterator([])()
const uut = new SearchQuery({ postsDb, postParentsDb, namesDb, profilesDb })
const result = await uut.searchPosts('hello')
assert.equal(result.length, 1)
assert.equal(result[0].txid, 'tx2')
})
it('should exclude posts that do not match the query', async () => {
postsDb.iterator = () => makeIterator([
['tx1', { addr: 'addr1', text: 'hello world', seen: 100, blockHeight: 600100 }]
])()
postParentsDb.iterator = () => makeIterator([])()
const uut = new SearchQuery({ postsDb, postParentsDb, namesDb, profilesDb })
const result = await uut.searchPosts('zzz')
assert.equal(result.length, 0)
})
it('should default blockHeight to 0 for a profile record without one', async () => {
namesDb.iterator = () => makeIterator([])()
profilesDb.iterator = () => makeIterator([
['addr1', { text: 'bitcoin cash', txid: 'tx1', seen: 100 }]
])()
const uut = new SearchQuery({ postsDb, postParentsDb, namesDb, profilesDb })
const result = await uut.searchProfiles('bitcoin')
assert.equal(result.length, 1)
assert.equal(result[0].blockHeight, 0)
})
it('should default blockHeight and seen to 0 when profile records lack them', async () => {
namesDb.iterator = () => makeIterator([
['addr1', { name: 'Alice', txid: 'tx1' }]
])()
profilesDb.iterator = () => makeIterator([
['addr1', { text: 'bitcoin cash' }]
])()
const uut = new SearchQuery({ postsDb, postParentsDb, namesDb, profilesDb })
const result = await uut.searchProfiles('bitcoin')
assert.equal(result.length, 1)
assert.equal(result[0].blockHeight, 0)
assert.equal(result[0].seen, 0)
})
it('should fall back to the profile txid when the name record has none', async () => {
namesDb.iterator = () => makeIterator([
['addr1', { name: 'Alice' }]
])()
profilesDb.iterator = () => makeIterator([
['addr1', { text: 'bitcoin cash', txid: 'tx2', seen: 100, blockHeight: 600100 }]
])()
const uut = new SearchQuery({ postsDb, postParentsDb, namesDb, profilesDb })
const result = await uut.searchProfiles('bitcoin')
assert.equal(result.length, 1)
assert.equal(result[0].txid, 'tx2')
})
})
@@ -71,5 +71,6 @@ describe('#SearchRESTController', () => {
assert.equal(ctx.throw.callCount, 1)
assert.equal(ctx.throw.firstCall.args[0], 500)
assert.equal(ctx.throw.firstCall.args[1], 'boom')
})
})
@@ -102,4 +102,34 @@ describe('#SearchAll', () => {
assert.equal(searchQuery.searchPosts.firstCall.args[0], 'hello')
assert.equal(searchQuery.searchProfiles.firstCall.args[0], 'hello')
})
it('should search for a single-character query', async () => {
searchQuery.searchPosts.resolves([
{ txid: 'tx1', addr: 'addr1', text: 'hello', seen: 100, blockHeight: 600100 }
])
searchQuery.searchProfiles.resolves([])
const result = await uut.execute({ q: 'h' })
assert.equal(result.posts.length, 1)
assert.equal(searchQuery.searchPosts.firstCall.args[0], 'h')
})
it('should report hasMore false when the page reaches the end', async () => {
searchQuery.searchPosts.resolves([
{ txid: 'tx1', addr: 'addr1', text: 'a', seen: 100, blockHeight: 600100 },
{ txid: 'tx2', addr: 'addr2', text: 'b', seen: 200, blockHeight: 600200 }
])
searchQuery.searchProfiles.resolves([
{ addr: 'addr1', name: 'Alice', text: 'bio', seen: 100, blockHeight: 600100 },
{ addr: 'addr2', name: 'Bob', text: 'bio', seen: 200, blockHeight: 600200 }
])
const result = await uut.execute({ q: 'test', limit: 4, offset: 0 })
assert.equal(result.posts.length, 2)
assert.equal(result.profiles.length, 2)
assert.equal(result.pagination.total, 4)
assert.equal(result.pagination.hasMore, false)
})
})