Commit 0a5f6fb
committed
fix(pull): preserve lastPulledHash on classifier short-circuits (hotfix for #38)
The drift-direction-classifier PR (#38) introduced a regression: classifier
short-circuit branches in `pullResourceType` silently dropped `lastPulledHash`
from state, breaking drift detection for any resource that ever returned a
non-clean direction.
## Root cause
`pull.ts:769` initializes `newStateSection` as an empty object for a full pull:
const newStateSection: Record<string, ResourceState> = resourceIds?.length
? { ...state[resourceType] }
: {}; // ← empty for full pulls
The three classifier short-circuit branches then did one of:
- `both-diverged` (`:818`) \u2014 NO upsertState call at all. Entry vanishes from
newStateSection. At end-of-loop, `state[type] = newStateSection` removes it
from state entirely.
- `dashboard-ahead` (`:870`) \u2014 `upsertState(newStateSection, id, { uuid })`.
Merge against undefined produces `{ uuid }`-only, dropping lastPulledHash
and lastPulledAt.
- `local-ahead` (`:882`) \u2014 same shape as dashboard-ahead.
Symptom on the next pull: classifier sees no baseline \u2192 `no-baseline` \u2192
content-drift detection silently disabled for that resource until something
writes a fresh baseline. The `--resolve=ours` and `--resolve=theirs` paths
masked the bug locally (they explicitly write full state entries), but bare
plain pulls and `--resolve=fail` did not.
Caught by the E2E both-diverged smoke test on mudflap-iform-test:
- TEST 1: plain pull on both-diverged \u2192 exit 1 (gate fires) \u2192 state for that
resource becomes `{ uuid }` only (lastPulledHash gone)
- TEST 2: --resolve=fail \u2192 exit 1 \u2192 same state loss
## Fix
Pass the existing baseline through at each short-circuit:
- both-diverged: assign the existing state entry verbatim into newStateSection
before `continue`. `resolveBothDivergedResources` will overwrite it later
if the operator passes --resolve.
- dashboard-ahead, local-ahead: include lastPulledHash + lastPulledAt in the
upsertState patch so the merge against an empty section produces the full
shape.
## Tests
4 new unit tests in tests/drift.test.ts \u00a7 Section J pin:
- Full patch survives empty newStateSection (the fix shape)
- Bare { uuid }-only patch DROPS lastPulledHash (the regression hazard \u2014
fails if someone reverts the caller)
- both-diverged direct assignment preserves all fields verbatim
- upsertState merge semantics still hold (union, not replacement)
Empirically verified end-to-end against mudflap-iform-test:
- Create scenario \u2192 baseline hash X recorded \u2192 manufacture both-diverged \u2192
plain pull \u2192 exit 1 \u2192 state still has lastPulledHash == X \u2705
## Why this wasn't caught in review
Two rounds of code-reviewer subagent passes on PR #38 verified contract
correctness of `classifyDrift`'s output (which direction maps to which return
value) but did NOT trace what each branch did to `newStateSection`. State
mutation per classifier branch should be an explicit review surface; will
add to future review briefs.
## Defer to follow-up
A full integration test using the pull-same-name-clobber.test.ts pattern
(spawn worker thread with HTTP stub, run pull-cmd.ts via spawnSync, assert
post-pull state) would lock this contract more rigorously. The unit tests
here pin the shape; the smoke + manual repro confirm the integration.
270/270 tests pass, tsc --noEmit clean.1 parent e4678c0 commit 0a5f6fb
2 files changed
Lines changed: 125 additions & 2 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
852 | 852 | | |
853 | 853 | | |
854 | 854 | | |
| 855 | + | |
| 856 | + | |
| 857 | + | |
| 858 | + | |
| 859 | + | |
| 860 | + | |
| 861 | + | |
| 862 | + | |
| 863 | + | |
| 864 | + | |
855 | 865 | | |
856 | 866 | | |
857 | 867 | | |
| |||
861 | 871 | | |
862 | 872 | | |
863 | 873 | | |
| 874 | + | |
| 875 | + | |
| 876 | + | |
| 877 | + | |
| 878 | + | |
| 879 | + | |
| 880 | + | |
| 881 | + | |
864 | 882 | | |
865 | 883 | | |
866 | 884 | | |
| |||
869 | 887 | | |
870 | 888 | | |
871 | 889 | | |
872 | | - | |
| 890 | + | |
| 891 | + | |
| 892 | + | |
| 893 | + | |
| 894 | + | |
873 | 895 | | |
874 | 896 | | |
875 | 897 | | |
| |||
881 | 903 | | |
882 | 904 | | |
883 | 905 | | |
884 | | - | |
| 906 | + | |
| 907 | + | |
| 908 | + | |
| 909 | + | |
| 910 | + | |
885 | 911 | | |
886 | 912 | | |
887 | 913 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
3 | 3 | | |
4 | 4 | | |
5 | 5 | | |
| 6 | + | |
6 | 7 | | |
| 8 | + | |
7 | 9 | | |
8 | 10 | | |
9 | 11 | | |
| |||
659 | 661 | | |
660 | 662 | | |
661 | 663 | | |
| 664 | + | |
| 665 | + | |
| 666 | + | |
| 667 | + | |
| 668 | + | |
| 669 | + | |
| 670 | + | |
| 671 | + | |
| 672 | + | |
| 673 | + | |
| 674 | + | |
| 675 | + | |
| 676 | + | |
| 677 | + | |
| 678 | + | |
| 679 | + | |
| 680 | + | |
| 681 | + | |
| 682 | + | |
| 683 | + | |
| 684 | + | |
| 685 | + | |
| 686 | + | |
| 687 | + | |
| 688 | + | |
| 689 | + | |
| 690 | + | |
| 691 | + | |
| 692 | + | |
| 693 | + | |
| 694 | + | |
| 695 | + | |
| 696 | + | |
| 697 | + | |
| 698 | + | |
| 699 | + | |
| 700 | + | |
| 701 | + | |
| 702 | + | |
| 703 | + | |
| 704 | + | |
| 705 | + | |
| 706 | + | |
| 707 | + | |
| 708 | + | |
| 709 | + | |
| 710 | + | |
| 711 | + | |
| 712 | + | |
| 713 | + | |
| 714 | + | |
| 715 | + | |
| 716 | + | |
| 717 | + | |
| 718 | + | |
| 719 | + | |
| 720 | + | |
| 721 | + | |
| 722 | + | |
| 723 | + | |
| 724 | + | |
| 725 | + | |
| 726 | + | |
| 727 | + | |
| 728 | + | |
| 729 | + | |
| 730 | + | |
| 731 | + | |
| 732 | + | |
| 733 | + | |
| 734 | + | |
| 735 | + | |
| 736 | + | |
| 737 | + | |
| 738 | + | |
| 739 | + | |
| 740 | + | |
| 741 | + | |
| 742 | + | |
| 743 | + | |
| 744 | + | |
| 745 | + | |
| 746 | + | |
| 747 | + | |
| 748 | + | |
| 749 | + | |
| 750 | + | |
| 751 | + | |
| 752 | + | |
| 753 | + | |
| 754 | + | |
| 755 | + | |
| 756 | + | |
| 757 | + | |
| 758 | + | |
0 commit comments