mirror of
https://github.com/stablyai/orca.git
synced 2026-09-22 08:02:28 +00:00
* refactor(store): unify the catalog structural-equality walks Three near-identical structural deep-equality walks had landed independently in the same window: areValuesEqual (#13744, repo-identity-reconcile.ts), areCatalogEntriesEqual (#13770, repos.ts — already folded into the first on this branch's base) and catalogValuesEqual (#13662, worktree-catalog-reconciliation.ts). All three walk plain records and arrays and fall back to reference equality for anything exotic. They are not interchangeable. Two axes genuinely differ, and each caller depends on its own side: - Own-key set. #13744/#13770 require equal own-key counts plus hasOwnProperty, so an absent key differs from a key present and holding `undefined`. #13662 compares the union of both sides' keys, so those are equal. The strict side is load-bearing: the repo/project merges branch on `'localWindowsRuntimePreference' in project` (repos-project-runtime.test.ts "clears stale local runtime preferences"), and projects are now reconciled with this comparator. The loose side is test-pinned by worktree-catalog-reconciliation.test.ts "reuses rows with equivalent nested catalog data", where a locally built row carries `optional: undefined` that the host omits. - Leaf comparison. #13744/#13770 use `===` (NaN never equal, 0 equals -0); #13662 uses `Object.is` (the reverse). So instead of picking a winner, src/shared/structural-value-equality.ts holds one walk parameterised by those two axes and exports the two policies as `structuralValuesEqual` and `structuralValuesEqualIgnoringUndefined`. Every caller keeps its exact current semantics; the ~40 duplicated lines and the silent divergence go away. src/shared/persisted-ui-equality.ts (a fourth copy with a Set branch and no plain-object guard) is deliberately left alone: it gates a disk write in main with no direct test coverage. Also folded, all provably behaviour-identical: - The `${hostId}\0${repoId}` composite key had three copies (getRepoHostIdentityForParts, repoOwnerKey, getEntryKey) that must agree or repos silently stop reconciling. Moved to src/shared/repo-host-identity.ts because one of them lives in src/shared; the renderer module re-exports it. - mergeFetchedReposForHost's hand-inlined upsert loop now calls mergeByIdentity. mergeByIdentity additionally skips replacing a structurally equal row, which cannot change the result here: reconcileFetchedRepos runs immediately after over the same identities in the same order and restores exactly those rows. - Renamed repos.ts's `catalogRowsUnchanged` to `arrayElementsUnchanged`. It is a pure element-identity compare, two files away from `catalogRowsEqual`, which is a full structural compare. src/shared/structural-value-equality.test.ts pins both policies over arrays, nested records, null-prototype records, absent-vs-undefined keys, symbol keys, and non-plain objects (Date/Map/Set/class) falling back to reference equality. * fix(store): keep merged sourceRepoIds order host-independent Prefixing the cross-host remainder made a cross-host project's sourceRepoIds order a function of the refreshing host, so the projects reconcile never reused the row. Also pins the repo-derived host-id contribution the new per-project slice feeds the host-id resolvers. Co-authored-by: Orca <help@stably.ai> * refactor(store): migrate call sites that landed after this branch github.ts and ai-vault-session-identity.ts began using areValuesEqual on main while this branch was stale, and repo-identity-reconcile's record reconciler still called its own deleted walker. All three now use structuralValuesEqual; reuseEqualCatalogRows keeps its duplicate-id cap and calls the ignoring-undefined variant, which is the key-union semantics catalogValuesEqual had. --------- Co-authored-by: Orca <help@stably.ai>
97 lines
4.1 KiB
TypeScript
97 lines
4.1 KiB
TypeScript
import { describe, expect, it } from 'vitest'
|
|
import {
|
|
structuralValuesEqual,
|
|
structuralValuesEqualIgnoringUndefined
|
|
} from './structural-value-equality'
|
|
|
|
const comparators = [
|
|
['structuralValuesEqual', structuralValuesEqual],
|
|
['structuralValuesEqualIgnoringUndefined', structuralValuesEqualIgnoringUndefined]
|
|
] as const
|
|
|
|
describe.each(comparators)('%s', (_name, valuesEqual) => {
|
|
it('compares primitives and identical references', () => {
|
|
expect(valuesEqual('a', 'a')).toBe(true)
|
|
expect(valuesEqual('a', 'b')).toBe(false)
|
|
expect(valuesEqual(null, null)).toBe(true)
|
|
expect(valuesEqual(null, {})).toBe(false)
|
|
expect(valuesEqual(undefined, null)).toBe(false)
|
|
const fn = (): void => {}
|
|
expect(valuesEqual(fn, fn)).toBe(true)
|
|
expect(valuesEqual(fn, (): void => {})).toBe(false)
|
|
})
|
|
|
|
it('walks arrays element-wise and never mixes them with records', () => {
|
|
expect(valuesEqual([1, [2, { a: 'b' }]], [1, [2, { a: 'b' }]])).toBe(true)
|
|
expect(valuesEqual([1, 2], [1, 2, 3])).toBe(false)
|
|
expect(valuesEqual([1, 2], [2, 1])).toBe(false)
|
|
expect(valuesEqual([], {})).toBe(false)
|
|
expect(valuesEqual({ 0: 1, length: 1 }, [1])).toBe(false)
|
|
})
|
|
|
|
it('walks nested plain records rebuilt by structured clone', () => {
|
|
const record = { id: 'a', nested: { labels: ['one', 'two'], flags: { on: true } } }
|
|
expect(valuesEqual(record, structuredClone(record))).toBe(true)
|
|
expect(
|
|
valuesEqual(record, { ...record, nested: { labels: ['one'], flags: { on: true } } })
|
|
).toBe(false)
|
|
})
|
|
|
|
it('walks null-prototype records like literals', () => {
|
|
const nullProto: Record<string, unknown> = Object.create(null)
|
|
nullProto.a = 1
|
|
expect(valuesEqual(nullProto, { a: 1 })).toBe(true)
|
|
expect(valuesEqual(nullProto, { a: 2 })).toBe(false)
|
|
})
|
|
|
|
it('falls back to reference equality for non-plain objects', () => {
|
|
const date = new Date(0)
|
|
expect(valuesEqual(date, date)).toBe(true)
|
|
expect(valuesEqual(date, new Date(0))).toBe(false)
|
|
expect(valuesEqual(new Map([['a', 1]]), new Map([['a', 1]]))).toBe(false)
|
|
expect(valuesEqual(new Set([1]), new Set([1]))).toBe(false)
|
|
class Row {
|
|
x = 1
|
|
}
|
|
expect(valuesEqual(new Row(), new Row())).toBe(false)
|
|
expect(valuesEqual(new Row(), { x: 1 })).toBe(false)
|
|
})
|
|
|
|
it('ignores symbol keys', () => {
|
|
const key = Symbol('marker')
|
|
expect(valuesEqual({ [key]: 1, a: 1 }, { [key]: 2, a: 1 })).toBe(true)
|
|
})
|
|
})
|
|
|
|
describe('structuralValuesEqual', () => {
|
|
it('treats an absent key as different from a key holding undefined', () => {
|
|
// Why: repo/project merges branch on `'key' in project`, so key presence is load-bearing.
|
|
expect(structuralValuesEqual({ a: 1 }, { a: 1, b: undefined })).toBe(false)
|
|
expect(structuralValuesEqual({ a: 1, b: undefined }, { a: 1 })).toBe(false)
|
|
expect(structuralValuesEqual({ a: 1, b: undefined }, { a: 1, c: undefined })).toBe(false)
|
|
expect(structuralValuesEqual({ a: 1, b: undefined }, { a: 1, b: undefined })).toBe(true)
|
|
})
|
|
|
|
it('compares leaves with === so NaN is never equal and -0 matches 0', () => {
|
|
expect(structuralValuesEqual({ a: Number.NaN }, { a: Number.NaN })).toBe(false)
|
|
expect(structuralValuesEqual({ a: 0 }, { a: -0 })).toBe(true)
|
|
})
|
|
})
|
|
|
|
describe('structuralValuesEqualIgnoringUndefined', () => {
|
|
it('treats an absent key as equal to a key holding undefined', () => {
|
|
// Why: locally built worktree rows carry explicit undefined fields the host omits.
|
|
expect(structuralValuesEqualIgnoringUndefined({ a: 1 }, { a: 1, b: undefined })).toBe(true)
|
|
expect(structuralValuesEqualIgnoringUndefined({ a: 1, b: undefined }, { a: 1 })).toBe(true)
|
|
expect(
|
|
structuralValuesEqualIgnoringUndefined({ a: 1, b: undefined }, { a: 1, c: undefined })
|
|
).toBe(true)
|
|
expect(structuralValuesEqualIgnoringUndefined({ a: 1 }, { a: 1, b: 2 })).toBe(false)
|
|
})
|
|
|
|
it('compares leaves with Object.is so NaN is equal and -0 differs from 0', () => {
|
|
expect(structuralValuesEqualIgnoringUndefined({ a: Number.NaN }, { a: Number.NaN })).toBe(true)
|
|
expect(structuralValuesEqualIgnoringUndefined({ a: 0 }, { a: -0 })).toBe(false)
|
|
})
|
|
})
|