---
id: TST-CRM-ENT-794
title: Record locks and revisions integration
kind: integration
product_area: crm
module: entities
revision: 1
status: draft
source_sha:
  services-crm: a8d1364f1dd44b723bb5f9acba12f983e9ec8d69
repository: services-crm
files:
  - modules/entities/tests/integration/db/change-events-capture.integration.spec.js
  - modules/entities/tests/integration/db/record-revisions.table.integration.spec.js
  - modules/entities/tests/integration/services/persistence/contacts.history.integration.spec.js
  - modules/entities/tests/integration/services/persistence/record-history-labels.queries.integration.spec.js
  - modules/entities/tests/integration/services/persistence/record-locks.integration.spec.js
  - modules/entities/tests/integration/services/persistence/record-locks.queries.integration.spec.js
  - modules/entities/tests/integration/services/persistence/record-revisions.queries.integration.spec.js
  - modules/entities/tests/integration/services/persistence/record-saves.no-op.integration.spec.js
implements:
  - VT-IMC-CRM-ENT-014-01
  - VT-IMC-CRM-ENT-011-01
  - ATS-CRM-ENT-016-04
  - ATS-CRM-ENT-016-01
  - ATS-CRM-ENT-015-02
  - ATS-CRM-ENT-015-03
  - ATS-CRM-ENT-015-05
  - ATS-CRM-ENT-015-06
  - ATS-CRM-ENT-015-08
  - ATS-CRM-ENT-016-05
  - ATS-CRM-ENT-004-02
  - FAS-CRM-ENT-07-05
environment: local
implementation_state: implemented
latest_result: not-run
latest_run_at: null
latest_run_ref: null
conformance: not-verified
estimated_minutes: null
estimate_basis: No measured duration is available for this cohesive group; per-file allowances are not runtime estimates.
isolation: >-
  Tests own the subjects, actors and revisions they create. Cleanup removes subject fixtures and their scoped
  historical data using the source-declared mechanism. No-op cases retain a baseline read so comparison uses
  stored versions and timestamps rather than the write response alone.
binding_scope: >-
  Bindings preserve the existing assertion contributions and exact source file. Composite scenarios still
  require every bound contribution and any unbound checks; consolidation does not certify conformance.
bindings:
  - scenario: ATS-CRM-ENT-016-04
    source: >-
      services-crm:modules/entities/tests/integration/db/record-revisions.table.integration.spec.js#may not
      insert a revision, so nothing the application runs can forge one
  - scenario: ATS-CRM-ENT-016-04
    source: >-
      services-crm:modules/entities/tests/integration/db/record-revisions.table.integration.spec.js#keeps a
      revision of a contact that has been deleted, the row surviving the record
  - scenario: ATS-CRM-ENT-016-04
    source: >-
      services-crm:modules/entities/tests/integration/db/record-revisions.table.integration.spec.js#reads
      nothing of another tenant’s, the policy hiding it even to a statement that names its record
  - scenario: ATS-CRM-ENT-016-01
    source: >-
      services-crm:modules/entities/tests/integration/services/persistence/contacts.history.integration.spec.js#writes
      exactly one revision, whatever the save touched
  - scenario: ATS-CRM-ENT-016-01
    source: >-
      services-crm:modules/entities/tests/integration/services/persistence/contacts.history.integration.spec.js#labels
      it with the contact’s display name, so a deleted contact stays identifiable
  - scenario: ATS-CRM-ENT-015-02
    source: >-
      services-crm:modules/entities/tests/integration/services/persistence/record-locks.integration.spec.js#refuses
      a stale version as stale
  - scenario: ATS-CRM-ENT-015-02
    source: >-
      services-crm:modules/entities/tests/integration/services/persistence/record-locks.integration.spec.js#refuses
      a blank reason, naming the reason
  - scenario: ATS-CRM-ENT-015-02
    source: >-
      services-crm:modules/entities/tests/integration/services/persistence/record-locks.integration.spec.js#refuses
      an unknown record as not found
  - scenario: ATS-CRM-ENT-015-02
    source: >-
      services-crm:modules/entities/tests/integration/services/persistence/record-locks.integration.spec.js#answers
      that a lock the record already has changed nothing
  - scenario: ATS-CRM-ENT-015-02
    source: >-
      services-crm:modules/entities/tests/integration/services/persistence/record-locks.integration.spec.js#leaves
      the role at its version and lock, and records nothing
  - scenario: ATS-CRM-ENT-015-03
    source: >-
      services-crm:modules/entities/tests/integration/services/persistence/record-locks.integration.spec.js#records
      who unlocked it, the lock it had and why, the reason trimmed
  - scenario: ATS-CRM-ENT-015-03
    source: >-
      services-crm:modules/entities/tests/integration/services/persistence/record-locks.integration.spec.js#records
      both changes, in order
  - scenario: ATS-CRM-ENT-015-05
    source: >-
      services-crm:modules/entities/tests/integration/services/persistence/record-locks.integration.spec.js#refuses
      an ordinary replacement while it is locked
  - scenario: ATS-CRM-ENT-015-06
    source: >-
      services-crm:modules/entities/tests/integration/services/persistence/record-locks.integration.spec.js#refuses
      an element id the organization does not have, releasing nothing
  - scenario: ATS-CRM-ENT-015-06
    source: >-
      services-crm:modules/entities/tests/integration/services/persistence/record-locks.integration.spec.js#audits
      each released element with the reason, though the contact itself was not locked
  - scenario: ATS-CRM-ENT-015-08
    source: >-
      services-crm:modules/entities/tests/integration/services/persistence/record-locks.integration.spec.js#refuses
      the unlock, naming the protection
  - scenario: ATS-CRM-ENT-015-08
    source: >-
      services-crm:modules/entities/tests/integration/services/persistence/record-locks.integration.spec.js#refuses
      a lock change too
  - scenario: ATS-CRM-ENT-015-08
    source: >-
      services-crm:modules/entities/tests/integration/services/persistence/record-locks.integration.spec.js#refuses
      an ordinary replacement that would drop its lock
  - scenario: ATS-CRM-ENT-015-08
    source: >-
      services-crm:modules/entities/tests/integration/services/persistence/record-locks.integration.spec.js#still
      lets the customer rename it, keeping its lock
  - scenario: ATS-CRM-ENT-015-08
    source: >-
      services-crm:modules/entities/tests/integration/services/persistence/record-locks.integration.spec.js#refuses
      the delete, naming the protection
  - scenario: ATS-CRM-ENT-015-03
    source: >-
      services-crm:modules/entities/tests/integration/services/persistence/record-locks.queries.integration.spec.js#answers
      the newest two changes of the record, every property mapped, and where the page ended
  - scenario: ATS-CRM-ENT-015-03
    source: >-
      services-crm:modules/entities/tests/integration/services/persistence/record-locks.queries.integration.spec.js#answers
      the oldest change on the page after, with no further page
  - scenario: ATS-CRM-ENT-016-05
    source: >-
      services-crm:modules/entities/tests/integration/services/persistence/record-revisions.queries.integration.spec.js#answers
      the display name the acting user has now, not the one the capture snapshotted
  - scenario: ATS-CRM-ENT-004-02
    source: >-
      services-crm:modules/entities/tests/integration/services/persistence/record-saves.no-op.integration.spec.js#answers
      the version it had
  - scenario: ATS-CRM-ENT-004-02
    source: >-
      services-crm:modules/entities/tests/integration/services/persistence/record-saves.no-op.integration.spec.js#keeps
      the stored version and last-modified stamp
  - scenario: ATS-CRM-ENT-004-02
    source: >-
      services-crm:modules/entities/tests/integration/services/persistence/record-saves.no-op.integration.spec.js#writes
      no revision
  - scenario: ATS-CRM-ENT-004-02
    source: >-
      services-crm:modules/entities/tests/integration/services/persistence/record-saves.no-op.integration.spec.js#is
      refused as stale, though the save changes nothing
  - scenario: ATS-CRM-ENT-016-01
    source: >-
      services-crm:modules/entities/tests/integration/services/persistence/record-saves.no-op.integration.spec.js#writes
      no revision
  - scenario: FAS-CRM-ENT-07-05
    source: >-
      services-crm:modules/entities/tests/integration/services/persistence/record-saves.no-op.integration.spec.js#writes
      no revision
    provenance: test
    scope: >-
      Partial: contact version, timestamp, revision absence and stale-save checks; organization version and
      timestamp checks. Organization history and stale refusal remain proposed: FND-CRM-ENT-076.
evidence:
  - 'services-crm:modules/entities/tests/integration/db/change-events-capture.integration.spec.js'
  - 'services-crm:modules/entities/tests/integration/db/record-revisions.table.integration.spec.js'
  - 'services-crm:modules/entities/tests/integration/services/persistence/contacts.history.integration.spec.js'
  - >-
    services-crm:modules/entities/tests/integration/services/persistence/record-history-labels.queries.integration.spec.js
  - 'services-crm:modules/entities/tests/integration/services/persistence/record-locks.integration.spec.js'
  - >-
    services-crm:modules/entities/tests/integration/services/persistence/record-locks.queries.integration.spec.js
  - >-
    services-crm:modules/entities/tests/integration/services/persistence/record-revisions.queries.integration.spec.js
  - >-
    services-crm:modules/entities/tests/integration/services/persistence/record-saves.no-op.integration.spec.js
source_observations:
  - provenance: test
    source: 'services-crm:modules/entities/tests/integration/db/change-events-capture.integration.spec.js'
    declared_test_titles:
      - 'change events: the capture on CRM’s tables (integration)'
      - the tables the migration put the triggers on
      - 'names five entities, one per stream'
      - captures the activity and each of the two collections it owns
      - captures the contact and each of the seven collections it owns
      - captures the organization and each of the two collections it owns
      - 'captures the recalculation run, whose own INSERT and continuation counter drive the region job'
      - >-
        captures the compliance reprocess run, whose own INSERT and continuation counter drive the compliance
        job
      - the columns the migrations left on the four event tables
      - .
      - 'found the four tables, so what follows is about all of them and not about an empty read'
      - 'gives each of them the one claim column, which keeps two dispatcher runs off the same row'
      - 'leaves none of them an attempt count, a backoff or a reason: the queue owns the retrying'
      - lets the application role write the claim and nothing else of an event
      - a contact created
      - records one event for the contact and everything created with it
      - 'has nothing in `before`: the contact did not exist'
      - carries the contact as it ends the transaction in `after`
      - 'names the actor who wrote, taken from `app.user_id`'
      - a contact changed in its own row
      - records one UPDATE event
      - names the changed column as the wire spells it
      - 'carries both sides of the change, so a handler needs no second read'
      - 'a contact whose primary organization is set, then cleared, then the ended affiliation reopened'
      - >-
        counts a row appearing (nothing becoming a value) as a change: the primary affiliation, with its
        organization
      - names the collection that went from nothing to a value
      - 'counts NULL becoming a value as a change: the end date the clearing set, the day before'
      - 'counts a value becoming NULL as a change too: the end date the reopening cleared'
      - a contact deleted
      - 'records one DELETE event, so a handler learns of a record it can no longer read'
      - carries the contact as it last was in `before`
      - 'has nothing in `after`: the row is gone'
      - a save that touches the contact row and three of its collections
      - 'records one event for the whole save, not one per table'
      - names the contact’s own changed column and each changed collection together
      - 'folds each changed child row into `related_changes`, under the collection it belongs to'
      - a save that changes only a collection
      - records an event although the contact’s own row did not change
      - 'names the collection as the changed field, which is what a handler filters on'
      - carries the changed phone numbers in `related_changes`
      - an organization save that touches its row and two of its collections
      - records one event on the organization stream
      - names the organization’s own changed column and each changed collection together
      - 'an organization whose preferred address is changed, then removed'
      - held a preferred address before the change (premise)
      - records an event naming the addresses as changed when the preferred address changes
      - >-
        carries both addresses of the switch in `related_changes`: the one that stopped being preferred and
        the one that became it
      - records an event naming the addresses as changed when the preferred address is removed
      - carries the removed address and the one promoted to preferred in `related_changes`
      - a save that leaves the contact as it was
      - held no event for the contact when the no-op began (premise)
      - 'records nothing a handler would have to decide to ignore: the settle deleted the event at commit'
      - a write that touches nothing but the row’s bookkeeping
      - finds the same read recording an event for a column the capture does watch (premise)
      - 'records no event: a column the system maintains is not a change anyone reacts to'
      - a transaction rolled back
      - fails the transaction (premise)
      - 'leaves no event behind: the event is written in the very transaction it describes'
      - 'left the contact as it was, so there was nothing to tell anyone about'
      - a write that went around every service
      - 'is captured all the same: the capture is in the database, not in the code that wrote'
      - names the actor of the connection that wrote it
      - 'a child row written on its own, with no statement against the record'
      - records an event for the organization that owns it
      - carries the new child row in `related_changes`
      - 'has nothing in `after`: no statement touched the organization’s own row'
    skipped_declarations: []
    dependency_summary: >-
      The prepared PostgreSQL database and its declared constraints, policies and transaction behavior are
      real dependencies.
  - provenance: test
    source: 'services-crm:modules/entities/tests/integration/db/record-revisions.table.integration.spec.js'
    declared_test_titles:
      - record_revisions.table (integration)
      - a row the database accepts
      - 'accepts a row that breaks no constraint, so the refusals below are the constraints and not the row'
      - 'gives the revision an id of its own kind, so a revision reads as one wherever it appears'
      - 'numbers it from the table’s identity, which is the order one record’s timeline is read in'
      - the checks the table makes
      - refuses an operation outside the closed list by record_revisions_operation_check
      - refuses a source outside the closed list by record_revisions_sys_source_check
      - refuses a changes document that is not an object by record_revisions_changes_object_check
      - >-
        refuses a sys_recorded_at carrying more than milliseconds by
        record_revisions_sys_recorded_at_precision_check
      - refuses a sys_transaction_started_at carrying more than milliseconds by its own precision check
      - refuses a second row with the same tenant and id by record_revisions_pk
      - what the table deliberately has not
      - 'reads the table’s constraints at all, so the emptiness below is pg_constraint and not a typo'
      - 'has no foreign key, so no cascade can take a revision away with the record it is about'
      - 'keeps a revision of a contact that has been deleted, the row surviving the record'
      - the grants the application role holds
      - 'reads a revision of its own tenant, so the refusals below are the grants and not an empty table'
      - 'reads nothing of another tenant’s, the policy hiding it even to a statement that names its record'
      - 'may not insert a revision, so nothing the application runs can forge one'
      - 'may not revise a revision, even one of its own tenant and even in a column nobody reads'
      - 'may not delete a revision, the tenant purge’s function being the only deletion there is'
      - 'may not truncate the table, which would erase every tenant’s history in one statement'
      - 'may not advance the sequence behind the identity either, so it cannot disturb a timeline’s order'
      - 'reads nothing with no tenant at all: a statement outside a scoped transaction fails closed'
      - the columns the system fills in
      - 'leaves one revision of the contact it saved, so the columns below are a row the settle wrote'
      - 'fills the instants, the actor and the source of that revision under their sys_ names'
      - 'carries the other three sys_ columns too, whatever this save had to put in them'
      - 'has every one of the eight sys_ columns, so none of the renames was missed'
      - has none of the names those columns carried before migration 0053
      - the indexes that serve the timeline
      - has exactly the primary key and the three indexes the access patterns need
      - >-
        indexes a data job\u2019s revisions by the record they belong to, which is how a job\u2019s results
        read them
      - 'orders one record’s timeline newest first in the index itself, so a page needs no sort'
      - 'indexes nothing by an expression, which row-level security would make unusable anyway'
    skipped_declarations: []
    dependency_summary: >-
      The prepared PostgreSQL database and its declared constraints, policies and transaction behavior are
      real dependencies.
  - provenance: test
    source: >-
      services-crm:modules/entities/tests/integration/services/persistence/contacts.history.integration.spec.js
    declared_test_titles:
      - a contact’s change history (integration)
      - a contact created through its service
      - 'writes exactly one revision, whatever the save touched'
      - 'calls it a creation, from the record row having no before'
      - 'files it under the record type the console reads, and the contact’s id'
      - 'labels it with the contact’s display name, so a deleted contact stays identifiable'
      - 'stamps it with the format of its changes document, so a later format is read beside this one'
      - 'says who made it, from the execution context and never from the request'
      - 'says where the change came from: a test context is a console caller'
      - >-
        records the record’s own fields with no before at all, which is how a reader tells “absent” from “not
        set”
      - 'drops the tenant and the id from the document, neither being a change'
      - 'drops the columns the stream’s history excludes, so no internal column reaches a permanent record'
      - 'records every collection the create filled, each as the children it added'
      - 'calls each child of a create an insertion, which is the entry’s own net operation'
      - 'names each child by its identity fields, so a reader sees the slot and not an id'
      - >-
        lists the record’s own changed fields and the name of every changed collection, which is the
        timeline’s filter
      - 'redacts nothing, the contact stream declaring no redacted column'
      - 'times it within milliseconds of the commit, and after the transaction began'
      - a save that changes one field of the record
      - writes one revision for the save
      - calls it an update
      - 'records the field that changed, with the value it had and the value it has'
      - 'records no collection, the save having touched no child'
      - 'names only what changed in changed_fields, the computed name included where the name changed with it'
      - a save that changes one child and nothing else
      - 'writes one revision, the record’s own row having changed only its bookkeeping'
      - records the collection that changed and no other
      - 'records one entry for the child that changed, and none for the child that did not'
      - 'calls it a change of the child, not an addition and not a removal'
      - records the child’s changed field as before and after
      - 'names the collection in changed_fields, which is how a timeline filters by it'
      - 'records no field of the record itself, a bookkeeping-only row being nobody’s change'
      - a save that adds one child and removes another
      - 'records one entry per child that changed, and none for the child kept as it was'
      - 'keeps the removed child’s values, which are all a reader will ever have of it'
      - 'gives the added child no before side, there being nothing it was'
      - 'a phone number edited in place, its type corrected (ruling D-22)'
      - 'records one entry, the edit of the phone it kept, under that phone’s id'
      - 'records the type and the number as before and after, not a removal and an addition'
      - a phone number replaced by another (ruling D-22)
      - records the removal of the phone it dropped and the addition of the new one
      - records the removal under the dropped phone’s id and its type
      - records the addition under a new id
      - a contact deleted through its service
      - writes one revision for the deletion
      - calls it a deletion
      - 'keeps the contact’s label, which is what a timeline of a record that is gone shows'
      - 'records the record’s fields as they were, with no after'
      - 'lists the children the cascade took, which before the BEFORE DELETE opener simply vanished'
      - 'calls each cascaded child a deletion, read from the row as it was'
      - 'keeps each cascaded child’s values from OLD, there being nowhere else left to read them'
      - a save that changes nothing
      - 'starts from no revision, so the emptiness below is the netting and not an empty table'
      - 'leaves no revision, a save that changed nothing net being nobody’s change'
      - a save that changes a value and changes it back
      - 'leaves no revision, a document with no changed field and no changed collection being nothing to keep'
      - >-
        leaves the contact with the value it started with, so the absence above is the netting and not a
        failed save
      - a transaction that rolls back
      - 'fails the transaction, so the absence below is the rollback and not a save that quietly worked'
      - 'leaves no revision: a change that did not commit has no history, by construction'
      - a write that does not say where it came from
      - 'is refused at commit, naming the record and what it could not record'
      - 'takes the business change down with it: the contact is as it was'
      - 'records nothing, there being no change to record'
      - 'an operator’s own write, declared as maintenance'
      - 'is accepted, the context having said who is making the change and why'
      - 'records it as maintenance, which is what distinguishes it from a person’s request'
      - 'names the operator, never a shared name'
      - 'names why the run happened, so a reader of the history knows what it was'
      - 'leaves the next write on the same pooled connection declaring itself again, nothing having leaked'
      - the actor’s name as it then was
      - 'snapshots the display name the acting user had, rather than leaving a reader to look one up'
      - 'keeps that name after the user row is gone, which is the whole reason it is snapshotted'
      - >-
        leaves the name empty for an actor with no user row at all, which is the truth rather than an
        invention
      - falls back to the first and last name when the user has no display name
      - the policy that lets the lookup see the acting user’s row
      - 'admits a console caller its own row, which is the row the capture asks for'
      - 'admits a job acting as staff, which reaches every row'
      - 'admits a portal caller its own row, through its own tenant'
      - 'hides the row from a caller it is not, so the admissions above are the policy and not an open table'
      - a child moved from one contact to another
      - writes a revision of the contact the child left
      - writes a revision of the contact the child joined
      - calls it a deletion on the contact it left
      - calls it an insertion on the contact it joined
      - 'carries none of the other contact’s rows on either revision, each record’s history being its own'
      - records the child’s values as they were on the contact it left
      - two contacts exchanging a child at once
      - 'commits both, neither being refused as a deadlock'
      - 'writes a revision of each contact, each naming its own record'
      - two saves of two contacts at once
      - 'writes a revision for each save, neither save coalescing into the other'
      - 'gives the two saves different transaction ids, so "what else did this save change" cannot confuse them'
      - 'orders them by the table’s identity, which is commit order for one record'
      - every record one save changed
      - 'writes one revision per record, not one for the transaction'
      - 'gives them the same transaction id, which is what "also changed in this save" reads'
      - a value the contact links from its organization (docs/affiliations/README.md §0b)
      - 'records nothing when the organization changes the value, the contact holding none of its own'
      - 'records one revision when the organization removes it, the row becoming disconnected'
      - 'records the last value, the change of source and why the source went in that revision'
      - history of a record of another tenant
      - >-
        is there for the tenant that owns the record, so the absence below is the policy and not a missing
        revision
      - 'is hidden from every other tenant, the policy admitting a row by its tenant alone'
      - 'an organization, whose stream keeps history since CRM-M1'
      - 'is written, so the revisions below are the settle’s history and not a failed save'
      - writes one revision of the create and one of the rename
      - 'names the organization in both, by its name when the revision was written'
      - 'records the rename as the name, from the created value to the new one'
    skipped_declarations: []
    dependency_summary: >-
      The prepared PostgreSQL database and its declared constraints, policies and transaction behavior are
      real dependencies.
  - provenance: test
    source: >-
      services-crm:modules/entities/tests/integration/services/persistence/record-history-labels.queries.integration.spec.js
    declared_test_titles:
      - record history labels read (integration)
      - reference_names
      - 'answers the name a record has now, and calls an active one active'
      - 'marks a record whose status is not active, which the page shows as “(inactive)”'
      - 'answers a record’s own number, which is what a caller writes “Name (#number)” with'
      - marks an archived configuration record by its own flag rather than a status
      - 'does not answer at all for a target the tenant no longer has, so the page can say it was deleted'
      - 'answers an empty map for no ids at all, without going to the database'
      - never reaches another tenant’s record of the same id
      - 'names every object a CRM reference can point at, and nothing else'
      - history_reference_names of users
      - accepts the users object as a target a history page can name
      - answers a live user by the display name the revision’s actor reads as
      - answers a deleted user by their first and last name
      - 'does not answer for an id no user has, and answers each id once'
      - 'answers an empty map for no ids at all, without going to the database'
      - answers a CRM target as `referenceNames` does
      - standard_field_label_overrides
      - 'keys each override by its object and its field, which is how the configuration looks one up'
      - answers the asking tenant’s own words and never another tenant’s
      - answers only for the objects asked about
      - 'answers an empty map for no objects at all, without going to the database'
    skipped_declarations: []
    dependency_summary: >-
      The prepared PostgreSQL database and its declared constraints, policies and transaction behavior are
      real dependencies.
  - provenance: test
    source: 'services-crm:modules/entities/tests/integration/services/persistence/record-locks.integration.spec.js'
    declared_test_titles:
      - record locks (integration)
      - 'a contact role locked against updates and deletes, unlocked (DEF-CRM-0321)'
      - refuses the replacement while the lock holds (premise)
      - answers that it changed the lock
      - 'releases the lock, bumps the version and stamps the unlocking administrator'
      - lets the role be replaced afterwards
      - 'records who unlocked it, the lock it had and why, the reason trimmed'
      - lets the role be deleted afterwards
      - 'a contact locked against updates and deletes, unlocked (DEF-CRM-0662)'
      - releases the lock at the next version
      - lets the contact be replaced and then deleted
      - records the unlock
      - 'an organization locked, then unlocked (DEF-CRM-0118)'
      - locks it against updates and deletes
      - refuses an ordinary replacement while it is locked
      - lets it be renamed once it is unlocked
      - 'records both changes, in order'
      - 'the refusals and the no-op, each of which writes nothing'
      - refuses a stale version as stale
      - 'refuses a blank reason, naming the reason'
      - refuses an unknown record as not found
      - answers that a lock the record already has changed nothing
      - 'leaves the role at its version and lock, and records nothing'
      - a protected system record
      - 'refuses the unlock, naming the protection'
      - refuses a lock change too
      - refuses an ordinary replacement that would drop its lock
      - 'still lets the customer rename it, keeping its lock'
      - records no lock change
      - a protected system record without a lock
      - 'refuses the delete, naming the protection'
      - keeps the role
      - a reorder of contact roles beside an unrelated locked one (DEF-CRM-0181)
      - 'saves the new order, numbering the locked role in its place and keeping its lock'
      - 'element locks released by the unlock (CRM-ACC-0035, CRM-ACC-0129)'
      - creates the contact with locked elements
      - 'refuses the clearing write without the unlock key, naming the element'
      - 'refuses a write that changes a locked element beyond its lock, even with the key'
      - 'clears the locks of the phone numbers in a write that changes nothing else, for the unlock key holder'
      - releases the locks of the remaining elements through the unlock action
      - 'audits each released element with the reason, though the contact itself was not locked'
      - lets the released elements be changed afterwards
      - 'element locks named in the unlock, and of an organization'
      - 'refuses an element id the organization does not have, releasing nothing'
      - releases only the named element
      - refuses clearing the other without the unlock key and allows it with the key
      - changes nothing when no element or record lock is left
    skipped_declarations: []
    dependency_summary: >-
      The prepared PostgreSQL database and its declared constraints, policies and transaction behavior are
      real dependencies.
  - provenance: test
    source: >-
      services-crm:modules/entities/tests/integration/services/persistence/record-locks.queries.integration.spec.js
    declared_test_titles:
      - record lock reads (integration)
      - list_record_lock_changes
      - 'answers the newest two changes of the record, every property mapped, and where the page ended'
      - 'answers the oldest change on the page after, with no further page'
      - answers nothing for a record with no changes
      - protection_of
      - answers why a protected record is protected
      - 'answers nothing for the same id of another concept, or in another tenant'
    skipped_declarations: []
    dependency_summary: >-
      The prepared PostgreSQL database and its declared constraints, policies and transaction behavior are
      real dependencies.
  - provenance: test
    source: >-
      services-crm:modules/entities/tests/integration/services/persistence/record-revisions.queries.integration.spec.js
    declared_test_titles:
      - record revisions read (integration)
      - 'list_revisions, the timeline'
      - 'answers every revision of the record, newest first, by its sequence'
      - 'maps every element of a revision the presenter reads, as the types it reads them in'
      - >-
        answers the sequence as a number and the transaction id as a string, not the bigint and xid8 of the
        row
      - 'answers the instant as an ISO 8601 timestamp in UTC, which is what the console shows'
      - answers no cursor when the page holds the record’s whole history
      - 'ends a full page with the cursor of the revision it ended at, and the next page continues from it'
      - does not repeat or skip a revision when the record is changed behind the page
      - 'refuses a cursor it did not answer, rather than reading it as a sequence'
      - answers nothing for a record with no history at all
      - never reaches another tenant’s revisions of a record with the same id
      - answers nothing for a record type it holds no revision of
      - the filters the drawer offers
      - 'arranged three revisions of three operations, which is what the filters select from'
      - selects one operation
      - 'selects one person, and answers nothing for a person who changed nothing'
      - 'selects one source, and answers nothing for a source the record was never changed through'
      - 'selects a date range, inclusive at both ends'
      - applies two filters together
      - 'latest_revision, what the page says about the record'
      - 'answers the newest revision, with the label the record then had'
      - answers nothing for a record with no history
      - 'get_revision, one revision of this record'
      - 'answers the revision, mapped exactly as the timeline maps it'
      - 'does not find a revision that belongs to another record, so the detail route answers 404'
      - does not find a revision id that exists in another tenant
      - 'the actor’s name, resolved by the read'
      - 'answers the display name the acting user has now, not the one the capture snapshotted'
      - 'still answers for a user who has been deleted, because former users are named too'
      - falls back to the name the capture snapshotted when no user row answers for the actor at all
      - answers the actor’s id whatever the name resolution found
      - the actor’s name across the staff / customer boundary
      - 'does not let the reader see the actor’s user row, which is the premise'
      - 'answers the name the actor has now in the timeline, not the snapshot'
      - answers the same name in the person filter
      - 'list_revision_actors, the person filter'
      - 'answers each person once, however many changes they made'
      - answers each person’s name and the source they last acted through
      - answers nobody for a record with no history
      - 'count_also_changed, what else the save changed'
      - 'counts the other records one save changed, and not the record asked about'
      - 'leaves a transaction that changed nothing else out of the map, which a caller reads as zero'
      - 'answers an empty map for no transactions at all, without going to the database'
      - 'counts each transaction of a page separately, in one read'
      - 'history_starts_at, the instant before which there is nothing to show'
      - 'answers the creation of a record whose history is complete, because nothing precedes it'
      - answers the instant capture was installed for a record that existed before its oldest revision
      - 'answers the same for a record with no revisions at all, so a reader is never told history is complete'
      - 'the revisions one data job wrote, and what happened since (integration)'
      - list_job_revisions
      - 'answers one revision per record the job changed, and no revision of anything else'
      - 'answers the job’s own change of a record, with the value it overwrote and the value it wrote'
      - 'answers the data job that made the change, which the history names as its source (DEF-CRM-0027)'
      - 'maps every element the plan reads, as the same stored revision the timeline answers'
      - answers nothing for a job that wrote no revision of this record type
      - never reaches another tenant’s revisions of a job of the same id
      - list_revisions_since_job
      - holds only the records something happened to after the job
      - 'leaves out a record the job wrote and nobody touched since, which is the one a rollback may put back'
      - leaves out a record changed after the job that the job never wrote at all
      - 'names every field changed since, with who made the latest change and when'
      - 'says of a record deleted since that it is gone, so a rollback has nothing to write to'
      - answers an empty map for a job nothing was written under
      - never summarises another tenant’s revisions of a job of the same id
      - the tenant data export’s reads of the record history (integration)
      - 'pages the tenant’s whole history by id, two at a time, without repeating or skipping a revision'
      - 'answers the revisions of both contacts, the creations and the rename'
      - maps each revision exactly as the timeline does
      - answers nothing recorded after the cut-off
      - never answers another tenant’s revisions
      - says the tenant has contact history
      - says the tenant has no activity history
      - answers no history for no record type at all
    skipped_declarations: []
    dependency_summary: >-
      The prepared PostgreSQL database and its declared constraints, policies and transaction behavior are
      real dependencies.
  - provenance: test
    source: >-
      services-crm:modules/entities/tests/integration/services/persistence/record-saves.no-op.integration.spec.js
    declared_test_titles:
      - a record save that changes nothing (integration)
      - a contact saved exactly as it was read
      - answers the version it had
      - answers the record as it was read
      - keeps the stored version and last-modified stamp
      - writes no revision
      - a contact saved with one changed field (the premise of the no-op cases)
      - bumps the version
      - writes one revision
      - a contact saved as read at a version it has moved on from
      - 'is refused as stale, though the save changes nothing'
      - an organization saved exactly as it was read
      - answers the version it had
      - keeps the stored version and last-modified stamp
      - an organization saved with one changed field (the premise of the no-op cases)
      - bumps the version
    skipped_declarations: []
    dependency_summary: >-
      The prepared PostgreSQL database and its declared constraints, policies and transaction behavior are
      real dependencies.
command: >-
  npx mocha --timeout 60000 'modules/entities/tests/integration/db/change-events-capture.integration.spec.js'
  'modules/entities/tests/integration/db/record-revisions.table.integration.spec.js'
  'modules/entities/tests/integration/services/persistence/contacts.history.integration.spec.js'
  'modules/entities/tests/integration/services/persistence/record-history-labels.queries.integration.spec.js'
  'modules/entities/tests/integration/services/persistence/record-locks.integration.spec.js'
  'modules/entities/tests/integration/services/persistence/record-locks.queries.integration.spec.js'
  'modules/entities/tests/integration/services/persistence/record-revisions.queries.integration.spec.js'
  'modules/entities/tests/integration/services/persistence/record-saves.no-op.integration.spec.js'
retries: none
proves_controls: []
product_intent: proposed
implementation_evidence: observed
narrative:
  status: enriched
---

# Record locks and revisions integration

## Verification Boundary

Lock transitions, append-only record revisions, history reads and no-op saves cross the real PostgreSQL transaction and history-capture boundary.

## What This Suite Proves

The tests establish retained lock changes, optimistic-version refusals, element unlock auditing, history ordering, cursor behavior and actor-name resolution. Revision table tests constrain the retained stream. No-op save tests compare stored versions and timestamps and distinguish unchanged saves from real writes. Job revision reads identify changes after a job for later rollback analysis.

## What It Does Not Prove

The history drawer and deployed user authorization are outside this group. Job revision reads provide evidence for the retained stream, not proof of an entire data-job rollback. Deleting a current record does not imply that its personal revision values are removed; retention remains an existing control gap.

## Level and Rationale

Capture must agree with committed record changes, including multi-record transactions. Real trigger and transaction behavior is necessary to prove that a refused write records nothing and an unchanged save does not create history. An isolated mapper cannot establish stable pagination under a later write.

## Real Dependencies and Test Doubles

The database, record write services, lock services and revision queries are real. User-name fixtures supply current, deleted and invisible-actor premises. Repository bootstrap prepares the application role and history attribution, so owner fixtures do not silently bypass the required actor context.

## Must Not Be Doubled

Keep history capture, lock checks, version comparisons and the revision store real. Cursor tests need actual newly inserted revisions behind a page. Actor reads must preserve visibility restrictions; a prefilled display name would bypass the attribution boundary the test intends to exercise.

## Fixtures and Isolation

Tests own the subjects, actors and revisions they create. Cleanup removes subject fixtures and their scoped historical data using the source-declared mechanism. No-op cases retain a baseline read so comparison uses stored versions and timestamps rather than the write response alone.

## Determinism and Retries

History expectations use generated fixture references and explicit sequence ordering. Date-filter tests specify inclusive ranges; cursor tests assert no repetition or omission after changes. Run without automatic retries because a retry could make an unstable pagination or capture sequence appear correct.

## How It Runs

Run the command in the frontmatter from the service repository after preparing the test database. It selects every file in this aggregate register under the repository Mocha bootstrap and runs serially. This is the aggregate selection of the integration gate; the complete repository integration command includes other modules and is wider than this register.

## Coverage Gaps

Locks and revisions are supporting records, not independently archived entities. Their retained stream and release transitions do not prove record archive or delete through the console. Existing exact-reason, concurrent-lock and deleted-subject history findings remain open until their particular assertions exist.
