feat(v2): add context fingerprint hash that ignores time-only fields
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
32ebe0d4f2
commit
30e532f811
@@ -0,0 +1,28 @@
|
||||
function stableStringify(value) {
|
||||
if (value === null || typeof value !== 'object') return JSON.stringify(value);
|
||||
if (Array.isArray(value)) return `[${value.map(stableStringify).join(',')}]`;
|
||||
const keys = Object.keys(value).sort();
|
||||
return `{${keys.map((k) => `${JSON.stringify(k)}:${stableStringify(value[k])}`).join(',')}}`;
|
||||
}
|
||||
|
||||
function djb2Hash(str) {
|
||||
let hash = 5381;
|
||||
for (let i = 0; i < str.length; i += 1) {
|
||||
hash = ((hash << 5) + hash + str.charCodeAt(i)) >>> 0;
|
||||
}
|
||||
return hash.toString(16).padStart(8, '0');
|
||||
}
|
||||
|
||||
const FINGERPRINT_FIELDS = ['status', 'tecnico_atribuido', 'categoria', 'entidade'];
|
||||
|
||||
function buildContextFingerprint(ticket) {
|
||||
const followups = (ticket.followups || []).map((f) => `${f.id}:${f.date}`).sort();
|
||||
const tasks = (ticket.tasks || []).map((t) => `${t.id}:${t.status}:${t.date}`).sort();
|
||||
const picked = {};
|
||||
for (const field of FINGERPRINT_FIELDS) picked[field] = ticket[field];
|
||||
picked.followups = followups;
|
||||
picked.tasks = tasks;
|
||||
return djb2Hash(stableStringify(picked));
|
||||
}
|
||||
|
||||
module.exports = { stableStringify, djb2Hash, buildContextFingerprint };
|
||||
@@ -0,0 +1,38 @@
|
||||
const { test } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const { buildContextFingerprint, stableStringify } = require('./hash');
|
||||
|
||||
function baseTicket(overrides) {
|
||||
return Object.assign({
|
||||
status: 2,
|
||||
tecnico_atribuido: 'Wagner',
|
||||
categoria: 'Rede',
|
||||
entidade: 'Matriz',
|
||||
followups: [{ id: 1, date: '2026-08-20 10:00:00' }],
|
||||
tasks: [{ id: 1, status: 1, date: '2026-08-19 09:00:00' }],
|
||||
dias_aberto: 10,
|
||||
dias_sem_interacao: 3,
|
||||
}, overrides);
|
||||
}
|
||||
|
||||
test('alterar apenas dias_aberto/dias_sem_interacao NAO muda o hash', () => {
|
||||
const a = buildContextFingerprint(baseTicket({ dias_aberto: 10, dias_sem_interacao: 3 }));
|
||||
const b = buildContextFingerprint(baseTicket({ dias_aberto: 11, dias_sem_interacao: 4 }));
|
||||
assert.equal(a, b);
|
||||
});
|
||||
|
||||
test('novo followup MUDA o hash', () => {
|
||||
const a = buildContextFingerprint(baseTicket({}));
|
||||
const b = buildContextFingerprint(baseTicket({ followups: [{ id: 1, date: '2026-08-20 10:00:00' }, { id: 2, date: '2026-08-21 08:00:00' }] }));
|
||||
assert.notEqual(a, b);
|
||||
});
|
||||
|
||||
test('mudanca de tecnico MUDA o hash', () => {
|
||||
const a = buildContextFingerprint(baseTicket({ tecnico_atribuido: 'Wagner' }));
|
||||
const b = buildContextFingerprint(baseTicket({ tecnico_atribuido: 'Eric' }));
|
||||
assert.notEqual(a, b);
|
||||
});
|
||||
|
||||
test('stableStringify ignora ordem de chaves', () => {
|
||||
assert.equal(stableStringify({ b: 1, a: 2 }), stableStringify({ a: 2, b: 1 }));
|
||||
});
|
||||
Reference in New Issue
Block a user