@indigoai-us/hq-cloud 6.15.24 → 6.15.26

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -5,6 +5,7 @@ import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
5
5
  import * as fs from "fs";
6
6
  import * as path from "path";
7
7
  import * as os from "os";
8
+ import * as crypto from "crypto";
8
9
  import { clearContextCache } from "../context.js";
9
10
  import { lockPathFor } from "../operation-lock.js";
10
11
  // Re-export node:fs as a mutable module so unreadable-link regressions can
@@ -13,6 +14,13 @@ vi.mock("fs", async (importOriginal) => {
13
14
  const actual = await importOriginal();
14
15
  return { ...actual };
15
16
  });
17
+ // Same mutable-namespace trick as `fs` above: `skip` is an interactive-only
18
+ // resolution, so the only way to exercise its arm is to stand in for the
19
+ // prompt. Re-exporting the real module keeps every other call site genuine.
20
+ vi.mock("./conflict.js", async (importOriginal) => {
21
+ const actual = await importOriginal();
22
+ return { ...actual };
23
+ });
16
24
  describe("sync mutation stdin boundary", () => {
17
25
  it("accepts only the exact sidecar command and emits one discriminated upsert result", async () => {
18
26
  expect(SYNC_MUTATION_STDIN_COMMAND).toEqual(["sync", "mutation", "--stdin-json"]);
@@ -115,6 +123,7 @@ vi.mock("./reindex.js", () => ({
115
123
  import { sync, reportNewFilesToNotify, applyRealtimeDeltaPage, isSyncMutationStdinCommand, runSyncMutationStdinJson, SYNC_MUTATION_STDIN_COMMAND, win32SymlinkFlavorBroken, } from "./sync.js";
116
124
  import { share } from "./share.js";
117
125
  import * as s3Module from "../s3.js";
126
+ import * as conflictModule from "./conflict.js";
118
127
  import { reindex } from "./reindex.js";
119
128
  import { readSyncProgress } from "../sync-progress.js";
120
129
  import { VaultAuthError } from "../vault-client.js";
@@ -135,6 +144,16 @@ function journalAt(journalPath) {
135
144
  .replace(/\.json$/, "");
136
145
  return readJournal(slug);
137
146
  }
147
+ /**
148
+ * The single `.conflict-` sidecar in a directory. Asserting through this rather
149
+ * than re-globbing inline keeps "exactly one was minted" enforced everywhere a
150
+ * test reaches for the preserved side.
151
+ */
152
+ function sidecarIn(dir) {
153
+ const found = fs.readdirSync(dir).filter((f) => f.includes(".conflict-"));
154
+ expect(found).toHaveLength(1);
155
+ return path.join(dir, found[0]);
156
+ }
138
157
  const mockConfig = {
139
158
  apiUrl: "https://vault-api.test",
140
159
  authToken: "test-jwt-token",
@@ -482,7 +501,7 @@ describe("sync", () => {
482
501
  const result = await sync({ vaultConfig: mockConfig, hqRoot: tmpDir });
483
502
  expect(result.filesDownloaded).toBe(2);
484
503
  });
485
- it("detects conflicts with local changes and keeps local on --on-conflict keep", async () => {
504
+ it("detects conflicts with local changes and adopts the cloud copy on --on-conflict keep", async () => {
486
505
  const companyDocs = path.join(tmpDir, "companies", "acme", "docs");
487
506
  fs.mkdirSync(companyDocs, { recursive: true });
488
507
  fs.writeFileSync(path.join(companyDocs, "handoff.md"), "local version");
@@ -506,10 +525,12 @@ describe("sync", () => {
506
525
  });
507
526
  expect(result.conflicts).toBe(1);
508
527
  expect(result.conflictPaths).toEqual(["docs/handoff.md"]);
509
- expect(result.filesSkipped).toBeGreaterThanOrEqual(1);
510
- expect(fs.readFileSync(path.join(companyDocs, "handoff.md"), "utf-8")).toBe("local version");
528
+ // `keep` adopts the cloud body at the original name…
529
+ expect(fs.readFileSync(path.join(companyDocs, "handoff.md"), "utf-8")).toBe("mock file content");
530
+ // …and the local one survives beside it.
531
+ expect(fs.readFileSync(sidecarIn(companyDocs), "utf-8")).toBe("local version");
511
532
  });
512
- it("conflict keep marks the entry localDiverges (journal-honesty: kept copy never matched the recorded remote etag)", async () => {
533
+ it("conflict skip marks the entry localDiverges (journal-honesty: the held copy never matched the recorded remote etag)", async () => {
513
534
  const companyDocs = path.join(tmpDir, "companies", "acme", "docs");
514
535
  fs.mkdirSync(companyDocs, { recursive: true });
515
536
  fs.writeFileSync(path.join(companyDocs, "handoff.md"), "local version");
@@ -525,17 +546,28 @@ describe("sync", () => {
525
546
  },
526
547
  },
527
548
  }));
528
- const result = await sync({
529
- company: "acme",
530
- onConflict: "keep",
531
- vaultConfig: mockConfig,
532
- hqRoot: tmpDir,
533
- });
549
+ // `keep` no longer holds, so the honesty flag now belongs to the arms that
550
+ // still do. `skip` is the interactive one and is exercised through the
551
+ // prompt stand-in.
552
+ const resolveSpy = vi
553
+ .spyOn(conflictModule, "resolveConflict")
554
+ .mockResolvedValue("skip");
555
+ let result;
556
+ try {
557
+ result = await sync({
558
+ company: "acme",
559
+ vaultConfig: mockConfig,
560
+ hqRoot: tmpDir,
561
+ });
562
+ }
563
+ finally {
564
+ resolveSpy.mockRestore();
565
+ }
534
566
  expect(result.conflicts).toBe(1);
535
567
  const journal = journalAt(journalPath);
536
568
  // remoteEtag still recorded so the conflict can't re-fire (#137 invariant)…
537
569
  expect(journal.files["docs/handoff.md"].remoteEtag).toBeTruthy();
538
- // …but the kept-local copy is flagged divergent so a later currency-gated
570
+ // …but the held local copy is flagged divergent so a later currency-gated
539
571
  // delete cannot fire on a journal that lies about local==remote.
540
572
  expect(journal.files["docs/handoff.md"].localDiverges).toBe(true);
541
573
  });
@@ -576,6 +608,249 @@ describe("sync", () => {
576
608
  expect(journal.files["docs/handoff.md"].remoteEtag).toBeTruthy();
577
609
  expect(journal.files["docs/handoff.md"].localDiverges).toBe(true);
578
610
  });
611
+ // --- `keep` semantics, revised 2026-08-21 -------------------------------
612
+ //
613
+ // `keep` now means "the CLOUD copy becomes the working copy; my local body is
614
+ // preserved beside it as a `.conflict-` sidecar." It used to mean the
615
+ // opposite — hold local, stamp the remote etag, arm `localDiverges` — and
616
+ // that combination was terminal: push refused the divergent local, pull saw
617
+ // the remote unchanged so it never replaced the entry, and every subsequent
618
+ // `keep` re-armed the flag. The file could converge in neither direction, and
619
+ // because the desktop app hardcodes `--on-conflict keep`, that was the
620
+ // default outcome for every real conflict.
621
+ //
622
+ // Electing the cloud body clears the divergence BY CONSTRUCTION — the entry
623
+ // then genuinely records what sits on disk — while the local bytes survive
624
+ // under the sidecar for the operator to re-apply. Nothing is destroyed; the
625
+ // working copy changes and the old one is renamed.
626
+ it("conflict keep takes the cloud body at the original path and preserves local in a `.conflict-` sidecar", async () => {
627
+ const companyDocs = path.join(tmpDir, "companies", "acme", "docs");
628
+ fs.mkdirSync(companyDocs, { recursive: true });
629
+ fs.writeFileSync(path.join(companyDocs, "handoff.md"), "local version");
630
+ vi.mocked(s3Module.listRemoteFiles).mockResolvedValueOnce([
631
+ { key: "docs/handoff.md", size: 42, lastModified: new Date(), etag: '"divergent"' },
632
+ ]);
633
+ fs.writeFileSync(journalPath, JSON.stringify({
634
+ version: "1",
635
+ lastSync: new Date().toISOString(),
636
+ files: {
637
+ "docs/handoff.md": {
638
+ hash: "old-hash-from-last-sync",
639
+ size: 20,
640
+ syncedAt: new Date(Date.now() - 3600000).toISOString(),
641
+ direction: "down",
642
+ },
643
+ },
644
+ }));
645
+ const result = await sync({
646
+ company: "acme",
647
+ onConflict: "keep",
648
+ vaultConfig: mockConfig,
649
+ hqRoot: tmpDir,
650
+ });
651
+ expect(result.conflicts).toBe(1);
652
+ expect(result.conflictPaths).toEqual(["docs/handoff.md"]);
653
+ // The cloud body is the working copy now.
654
+ expect(fs.readFileSync(path.join(companyDocs, "handoff.md"), "utf-8")).toBe("mock file content");
655
+ // …and the local body is not lost — it is sitting beside it.
656
+ const sidecars = fs
657
+ .readdirSync(companyDocs)
658
+ .filter((f) => f.includes(".conflict-"));
659
+ expect(sidecars).toHaveLength(1);
660
+ expect(fs.readFileSync(path.join(companyDocs, sidecars[0]), "utf-8")).toBe("local version");
661
+ });
662
+ it("conflict keep clears the fetched sidecar before rename for Windows compatibility", async () => {
663
+ const companyDocs = path.join(tmpDir, "companies", "acme", "docs");
664
+ fs.mkdirSync(companyDocs, { recursive: true });
665
+ const originalPath = path.join(companyDocs, "handoff.md");
666
+ fs.writeFileSync(originalPath, "local version");
667
+ vi.mocked(s3Module.listRemoteFiles).mockResolvedValueOnce([
668
+ { key: "docs/handoff.md", size: 42, lastModified: new Date(), etag: '"divergent"' },
669
+ ]);
670
+ fs.writeFileSync(journalPath, JSON.stringify({
671
+ version: "1",
672
+ lastSync: new Date().toISOString(),
673
+ files: {
674
+ "docs/handoff.md": {
675
+ hash: "old-hash-from-last-sync",
676
+ size: 20,
677
+ syncedAt: new Date(Date.now() - 3600000).toISOString(),
678
+ direction: "down",
679
+ },
680
+ },
681
+ }));
682
+ // Node's Windows rename does not replace an occupied destination. Model
683
+ // that rule only for the local→sidecar move; unrelated atomic index and
684
+ // journal renames keep using the host filesystem's normal behaviour.
685
+ const realRenameSync = fs.renameSync;
686
+ const renameSpy = vi.spyOn(fs, "renameSync").mockImplementation((from, to) => {
687
+ if (String(from) === originalPath &&
688
+ String(to).includes(".conflict-") &&
689
+ fs.existsSync(to)) {
690
+ throw errnoError("EEXIST");
691
+ }
692
+ return realRenameSync(from, to);
693
+ });
694
+ try {
695
+ await sync({
696
+ company: "acme",
697
+ onConflict: "keep",
698
+ vaultConfig: mockConfig,
699
+ hqRoot: tmpDir,
700
+ });
701
+ }
702
+ finally {
703
+ renameSpy.mockRestore();
704
+ }
705
+ expect(fs.readFileSync(originalPath, "utf-8")).toBe("mock file content");
706
+ expect(fs.readFileSync(sidecarIn(companyDocs), "utf-8")).toBe("local version");
707
+ });
708
+ it("conflict keep converges the journal on the REMOTE body and leaves localDiverges unset", async () => {
709
+ const companyDocs = path.join(tmpDir, "companies", "acme", "docs");
710
+ fs.mkdirSync(companyDocs, { recursive: true });
711
+ fs.writeFileSync(path.join(companyDocs, "handoff.md"), "local version");
712
+ vi.mocked(s3Module.listRemoteFiles).mockResolvedValueOnce([
713
+ { key: "docs/handoff.md", size: 42, lastModified: new Date(), etag: '"divergent"' },
714
+ ]);
715
+ fs.writeFileSync(journalPath, JSON.stringify({
716
+ version: "1",
717
+ lastSync: new Date().toISOString(),
718
+ files: {
719
+ "docs/handoff.md": {
720
+ hash: "old-hash-from-last-sync",
721
+ size: 20,
722
+ syncedAt: new Date(Date.now() - 3600000).toISOString(),
723
+ direction: "down",
724
+ },
725
+ },
726
+ }));
727
+ await sync({
728
+ company: "acme",
729
+ onConflict: "keep",
730
+ vaultConfig: mockConfig,
731
+ hqRoot: tmpDir,
732
+ });
733
+ const entry = journalAt(journalPath).files["docs/handoff.md"];
734
+ expect(entry.remoteEtag).toBeTruthy();
735
+ // The whole point of the change: the entry describes the body that is
736
+ // actually on disk, so the honesty flag has nothing to report. A set flag
737
+ // here would re-create the deadlock this test exists to prevent.
738
+ expect(entry.localDiverges).toBeFalsy();
739
+ });
740
+ it("conflict keep records the REMOTE body's hash, so the entry is pushable instead of permanently divergent", async () => {
741
+ // This is the deadlock in one assertion. The old `keep` wrote the LOCAL
742
+ // hash next to the REMOTE etag, which is a self-contradictory entry: the
743
+ // push leg refuses to send a body the journal calls divergent, and the pull
744
+ // leg sees a remote whose etag already matches and leaves it alone. Writing
745
+ // the remote hash is what makes the entry describable — and therefore
746
+ // sendable — again.
747
+ const companyDocs = path.join(tmpDir, "companies", "acme", "docs");
748
+ fs.mkdirSync(companyDocs, { recursive: true });
749
+ fs.writeFileSync(path.join(companyDocs, "handoff.md"), "local version");
750
+ vi.mocked(s3Module.listRemoteFiles).mockResolvedValueOnce([
751
+ { key: "docs/handoff.md", size: 42, lastModified: new Date(), etag: '"divergent"' },
752
+ ]);
753
+ fs.writeFileSync(journalPath, JSON.stringify({
754
+ version: "1",
755
+ lastSync: new Date().toISOString(),
756
+ files: {
757
+ "docs/handoff.md": {
758
+ hash: "old-hash-from-last-sync",
759
+ size: 20,
760
+ syncedAt: new Date(Date.now() - 3600000).toISOString(),
761
+ direction: "down",
762
+ },
763
+ },
764
+ }));
765
+ await sync({
766
+ company: "acme",
767
+ onConflict: "keep",
768
+ vaultConfig: mockConfig,
769
+ hqRoot: tmpDir,
770
+ });
771
+ // Pinned to the REMOTE body specifically, not to "whatever is on disk" —
772
+ // the old behaviour also left those two agreeing (it recorded the local
773
+ // hash and left the local file in place), so a disk-relative assertion
774
+ // would pass against the very bug this guards.
775
+ const remoteHash = crypto
776
+ .createHash("sha256")
777
+ .update("mock file content")
778
+ .digest("hex");
779
+ const entry = journalAt(journalPath).files["docs/handoff.md"];
780
+ expect(entry.hash).toBe(remoteHash);
781
+ });
782
+ it("conflict keep indexes the sidecar as the LOCAL body so /resolve-conflicts offers the right side", async () => {
783
+ const companyDocs = path.join(tmpDir, "companies", "acme", "docs");
784
+ fs.mkdirSync(companyDocs, { recursive: true });
785
+ fs.writeFileSync(path.join(companyDocs, "handoff.md"), "local version");
786
+ vi.mocked(s3Module.listRemoteFiles).mockResolvedValueOnce([
787
+ { key: "docs/handoff.md", size: 42, lastModified: new Date(), etag: '"divergent"' },
788
+ ]);
789
+ fs.writeFileSync(journalPath, JSON.stringify({
790
+ version: "1",
791
+ lastSync: new Date().toISOString(),
792
+ files: {
793
+ "docs/handoff.md": {
794
+ hash: "old-hash-from-last-sync",
795
+ size: 20,
796
+ syncedAt: new Date(Date.now() - 3600000).toISOString(),
797
+ direction: "down",
798
+ },
799
+ },
800
+ }));
801
+ await sync({
802
+ company: "acme",
803
+ onConflict: "keep",
804
+ vaultConfig: mockConfig,
805
+ hqRoot: tmpDir,
806
+ });
807
+ const index = JSON.parse(fs.readFileSync(path.join(tmpDir, ".hq-conflicts", "index.json"), "utf-8"));
808
+ expect(index.conflicts).toHaveLength(1);
809
+ const entry = index.conflicts[0];
810
+ expect(entry.originalPath).toBe("companies/acme/docs/handoff.md");
811
+ expect(entry.preserved).toBe("local");
812
+ // The sidecar it points at holds the LOCAL body — the side the operator
813
+ // may want back. Reading the recorded path is the contract /resolve-conflicts
814
+ // relies on, so assert through it rather than re-globbing the directory.
815
+ expect(fs.readFileSync(path.join(tmpDir, entry.conflictPath), "utf-8")).toBe("local version");
816
+ });
817
+ it("interactive `skip` still leaves local in place — skip is not keep", async () => {
818
+ // `skip` means "do not touch this one, I will deal with it later". It is a
819
+ // separate arm from `keep` and must NOT adopt the cloud body; a strategy
820
+ // that says "leave it alone" may not rename the operator's file.
821
+ const companyDocs = path.join(tmpDir, "companies", "acme", "docs");
822
+ fs.mkdirSync(companyDocs, { recursive: true });
823
+ fs.writeFileSync(path.join(companyDocs, "handoff.md"), "local version");
824
+ vi.mocked(s3Module.listRemoteFiles).mockResolvedValueOnce([
825
+ { key: "docs/handoff.md", size: 42, lastModified: new Date(), etag: '"divergent"' },
826
+ ]);
827
+ fs.writeFileSync(journalPath, JSON.stringify({
828
+ version: "1",
829
+ lastSync: new Date().toISOString(),
830
+ files: {
831
+ "docs/handoff.md": {
832
+ hash: "old-hash-from-last-sync",
833
+ size: 20,
834
+ syncedAt: new Date(Date.now() - 3600000).toISOString(),
835
+ direction: "down",
836
+ },
837
+ },
838
+ }));
839
+ const resolveSpy = vi
840
+ .spyOn(conflictModule, "resolveConflict")
841
+ .mockResolvedValue("skip");
842
+ try {
843
+ await sync({
844
+ company: "acme",
845
+ vaultConfig: mockConfig,
846
+ hqRoot: tmpDir,
847
+ });
848
+ }
849
+ finally {
850
+ resolveSpy.mockRestore();
851
+ }
852
+ expect(fs.readFileSync(path.join(companyDocs, "handoff.md"), "utf-8")).toBe("local version");
853
+ });
579
854
  it("a genuine download clears a stale localDiverges flag (entry is now truly local==remote)", async () => {
580
855
  const companyDocs = path.join(tmpDir, "companies", "acme", "docs");
581
856
  fs.mkdirSync(companyDocs, { recursive: true });
@@ -835,13 +1110,10 @@ describe("sync", () => {
835
1110
  });
836
1111
  expect(result.conflicts).toBe(1);
837
1112
  expect(result.conflictPaths).toEqual(["docs/handoff.md"]);
838
- // Local preserved on keep.
839
- expect(fs.readFileSync(path.join(companyDocs, "handoff.md"), "utf-8")).toBe("local version");
840
- // Mirror retained for inspection.
841
- const litter = fs
842
- .readdirSync(companyDocs)
843
- .filter((f) => f.includes(".conflict-"));
844
- expect(litter).toHaveLength(1);
1113
+ // Cloud body adopted at the original name on keep…
1114
+ expect(fs.readFileSync(path.join(companyDocs, "handoff.md"), "utf-8")).toBe("mock file content");
1115
+ // …and exactly one sidecar retained, holding the displaced local body.
1116
+ expect(fs.readFileSync(sidecarIn(companyDocs), "utf-8")).toBe("local version");
845
1117
  });
846
1118
  it("cloud-authoritative file is PULL-WINS on conflict: overwrites local, no mirror, no false-stamp", async () => {
847
1119
  // company-brief.md is server-regenerated (gardener), so a divergence is not
@@ -954,10 +1226,12 @@ describe("sync", () => {
954
1226
  // Surfaced as a conflict, NOT a silent download.
955
1227
  expect(result.conflicts).toBe(1);
956
1228
  expect(result.conflictPaths).toEqual(["docs/handoff.md"]);
957
- expect(result.filesDownloaded).toBe(0);
958
- // The newer local bytes are intact — nothing was rolled back.
959
- expect(fs.readFileSync(localPath, "utf-8")).toBe(localContent);
960
- // Remote preserved as an inspection mirror rather than overwriting local.
1229
+ expect(result.filesDownloaded).toBe(1);
1230
+ // The newer local bytes are not lost — the rollback is survivable because
1231
+ // `keep` renames them aside rather than discarding them. The guard's job is
1232
+ // to route this through conflict resolution instead of a silent download;
1233
+ // which side ends up at the original name is the strategy's call.
1234
+ expect(fs.readFileSync(sidecarIn(path.dirname(localPath)), "utf-8")).toBe(localContent);
961
1235
  const litter = fs
962
1236
  .readdirSync(companyDocs)
963
1237
  .filter((f) => f.includes(".conflict-"));
@@ -1017,8 +1291,8 @@ describe("sync", () => {
1017
1291
  });
1018
1292
  expect(result.conflicts).toBe(1);
1019
1293
  expect(result.conflictPaths).toEqual(["docs/handoff.md"]);
1020
- expect(result.filesDownloaded).toBe(0);
1021
- expect(fs.readFileSync(localPath, "utf-8")).toBe(localEdit);
1294
+ expect(result.filesDownloaded).toBe(1);
1295
+ expect(fs.readFileSync(sidecarIn(path.dirname(localPath)), "utf-8")).toBe(localEdit);
1022
1296
  });
1023
1297
  it("anti-rollback guard is direction-gated: a SMALLER remote on a clean CONSUMER copy (direction=down) downloads, not conflict", async () => {
1024
1298
  // The device only ever PULLED this file (direction: "down") — a consumer
@@ -1142,18 +1416,16 @@ describe("sync", () => {
1142
1416
  // Surfaced as a conflict, NOT a silent download.
1143
1417
  expect(result.conflicts).toBe(1);
1144
1418
  expect(result.conflictPaths).toEqual(["docs/handoff.md"]);
1145
- expect(result.filesDownloaded).toBe(0);
1146
- // The local edit is intact.
1147
- expect(fs.readFileSync(localPath, "utf-8")).toBe(localContent);
1148
- // Cloud copy preserved alongside it for inspection / `/resolve-conflicts`.
1149
- const litter = fs
1150
- .readdirSync(companyDocs)
1151
- .filter((f) => f.includes(".conflict-"));
1152
- expect(litter).toHaveLength(1);
1153
- expect(fs.readFileSync(path.join(companyDocs, litter[0]), "utf-8")).toBe("mock file content");
1419
+ expect(result.filesDownloaded).toBe(1);
1420
+ // The unjournaled local body is not lost — it is renamed aside, which is
1421
+ // what makes adopting the cloud copy survivable for a file that had no
1422
+ // sync history at all.
1423
+ expect(fs.readFileSync(sidecarIn(path.dirname(localPath)), "utf-8")).toBe(localContent);
1424
+ // The cloud body took the original name.
1425
+ expect(fs.readFileSync(localPath, "utf-8")).toBe("mock file content");
1154
1426
  // And a durable record exists so the overwrite can never be silent.
1155
1427
  const index = JSON.parse(fs.readFileSync(path.join(tmpDir, ".hq-conflicts", "index.json"), "utf-8"));
1156
- expect(index.conflicts.some((c) => c.originalPath === "companies/acme/docs/handoff.md")).toBe(true);
1428
+ expect(index.conflicts.some((c) => c.originalPath === "companies/acme/docs/handoff.md" && c.preserved === "local")).toBe(true);
1157
1429
  });
1158
1430
  it("untracked-local guard is free on first pull: an unjournaled local file that already MATCHES the cloud reconciles silently", async () => {
1159
1431
  // The guard must not manufacture conflicts for the common case — a fresh
@@ -1803,9 +2075,10 @@ describe("sync", () => {
1803
2075
  });
1804
2076
  expect(result.conflicts).toBe(1);
1805
2077
  expect(result.conflictPaths).toEqual(["workspace/threads/handoff.json"]);
1806
- expect(result.filesSkipped).toBeGreaterThanOrEqual(1);
1807
- // B's newer local pointer is preserved verbatim — not clobbered by A's.
1808
- const kept = JSON.parse(fs.readFileSync(path.join(threadsLocal, "handoff.json"), "utf-8"));
2078
+ // B's newer local pointer is preserved verbatim — renamed aside rather than
2079
+ // destroyed. DEV-1778 was about the pointer being LOST; a recoverable
2080
+ // sidecar satisfies that, and the machine can still resume from it.
2081
+ const kept = JSON.parse(fs.readFileSync(sidecarIn(threadsLocal), "utf-8"));
1809
2082
  expect(kept.message).toBe("newer local from B");
1810
2083
  expect(kept.thread_path).toBe("workspace/threads/T-machineB.json");
1811
2084
  });
@@ -2307,10 +2580,16 @@ describe("sync", () => {
2307
2580
  },
2308
2581
  pulls: [],
2309
2582
  }));
2310
- vi.mocked(s3Module.headRemoteFile).mockResolvedValueOnce({
2583
+ vi.mocked(s3Module.headRemoteFile)
2584
+ .mockResolvedValueOnce({
2311
2585
  etag: "old-etag",
2312
2586
  size: baseline.length,
2313
2587
  lastModified: new Date(),
2588
+ })
2589
+ .mockResolvedValueOnce({
2590
+ etag: "peer-etag",
2591
+ size: "mock file content".length,
2592
+ lastModified: new Date(),
2314
2593
  });
2315
2594
  const preconditionError = Object.assign(new Error("remote changed"), {
2316
2595
  name: "PreconditionFailed",
@@ -2326,7 +2605,15 @@ describe("sync", () => {
2326
2605
  onConflict: "keep",
2327
2606
  });
2328
2607
  expect(result.conflictPaths).toContain("board.json");
2329
- expect(fs.readFileSync(boardPath, "utf-8")).toBe(localAppend);
2608
+ // The conditional PUT still fails closed, but keep is cloud-wins: the
2609
+ // peer body takes the working name and the local append remains in the
2610
+ // sidecar for explicit resolution.
2611
+ expect(fs.readFileSync(boardPath, "utf-8")).toBe("mock file content");
2612
+ expect(fs.readFileSync(sidecarIn(companyRoot), "utf-8")).toBe(localAppend);
2613
+ expect(journalAt(journalPath).files["board.json"]).toMatchObject({
2614
+ remoteEtag: "peer-etag",
2615
+ direction: "down",
2616
+ });
2330
2617
  expect(s3Module.uploadFile).toHaveBeenCalledWith(expect.anything(), boardPath, "board.json", undefined, { ifMatch: "old-etag" });
2331
2618
  });
2332
2619
  it("does NOT tombstone symlinks whose readlink target has diverged from the journal (Codex P1 round 4)", async () => {
@@ -2604,7 +2891,7 @@ describe("sync", () => {
2604
2891
  expect(result.conflicts).toBeGreaterThanOrEqual(1);
2605
2892
  expect(result.conflictPaths).toContain("docs/edited.md");
2606
2893
  expect(fs.existsSync(editedPath)).toBe(true);
2607
- expect(fs.readFileSync(editedPath, "utf-8")).toBe("my local edits");
2894
+ expect(fs.readFileSync(sidecarIn(path.dirname(editedPath)), "utf-8")).toBe("my local edits");
2608
2895
  expect(result.filesTombstoned).toBe(0);
2609
2896
  });
2610
2897
  it("delete-resync: suppression is per-key and the fetch is company-scoped (no cross-tenant bleed)", async () => {