@indigoai-us/hq-cloud 6.14.17 → 6.14.19
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.
- package/.github/workflows/ci.yml +35 -0
- package/dist/active-company.d.ts +43 -0
- package/dist/active-company.d.ts.map +1 -0
- package/dist/active-company.js +132 -0
- package/dist/active-company.js.map +1 -0
- package/dist/active-company.test.d.ts +2 -0
- package/dist/active-company.test.d.ts.map +1 -0
- package/dist/active-company.test.js +149 -0
- package/dist/active-company.test.js.map +1 -0
- package/dist/bin/sync-runner-company.d.ts +7 -0
- package/dist/bin/sync-runner-company.d.ts.map +1 -1
- package/dist/bin/sync-runner-company.js +21 -1
- package/dist/bin/sync-runner-company.js.map +1 -1
- package/dist/bin/sync-runner-planning.d.ts +27 -1
- package/dist/bin/sync-runner-planning.d.ts.map +1 -1
- package/dist/bin/sync-runner-planning.js +33 -4
- package/dist/bin/sync-runner-planning.js.map +1 -1
- package/dist/bin/sync-runner-planning.test.d.ts +2 -0
- package/dist/bin/sync-runner-planning.test.d.ts.map +1 -0
- package/dist/bin/sync-runner-planning.test.js +115 -0
- package/dist/bin/sync-runner-planning.test.js.map +1 -0
- package/dist/bin/sync-runner-watch-loop.d.ts +10 -2
- package/dist/bin/sync-runner-watch-loop.d.ts.map +1 -1
- package/dist/bin/sync-runner-watch-loop.js +201 -25
- package/dist/bin/sync-runner-watch-loop.js.map +1 -1
- package/dist/bin/sync-runner-watch-routes.d.ts +1 -1
- package/dist/bin/sync-runner-watch-routes.d.ts.map +1 -1
- package/dist/bin/sync-runner-watch-routes.js +14 -2
- package/dist/bin/sync-runner-watch-routes.js.map +1 -1
- package/dist/bin/sync-runner.d.ts +39 -9
- package/dist/bin/sync-runner.d.ts.map +1 -1
- package/dist/bin/sync-runner.js +114 -20
- package/dist/bin/sync-runner.js.map +1 -1
- package/dist/bin/sync-runner.test.js +852 -8
- package/dist/bin/sync-runner.test.js.map +1 -1
- package/dist/cli/rescue-core.js +27 -1
- package/dist/cli/rescue-core.js.map +1 -1
- package/dist/cli/rescue-drop-dir-symlink.test.d.ts +2 -0
- package/dist/cli/rescue-drop-dir-symlink.test.d.ts.map +1 -0
- package/dist/cli/rescue-drop-dir-symlink.test.js +206 -0
- package/dist/cli/rescue-drop-dir-symlink.test.js.map +1 -0
- package/dist/cli/share.d.ts +15 -0
- package/dist/cli/share.d.ts.map +1 -1
- package/dist/cli/share.js +100 -55
- package/dist/cli/share.js.map +1 -1
- package/dist/cli/share.test.js +119 -6
- package/dist/cli/share.test.js.map +1 -1
- package/dist/cli/sync.d.ts.map +1 -1
- package/dist/cli/sync.js +19 -25
- package/dist/cli/sync.js.map +1 -1
- package/dist/cli/sync.test.js +121 -0
- package/dist/cli/sync.test.js.map +1 -1
- package/dist/journal.d.ts +3 -1
- package/dist/journal.d.ts.map +1 -1
- package/dist/journal.js +13 -2
- package/dist/journal.js.map +1 -1
- package/dist/journal.test.js +26 -1
- package/dist/journal.test.js.map +1 -1
- package/dist/manifest-reconcile.d.ts +118 -5
- package/dist/manifest-reconcile.d.ts.map +1 -1
- package/dist/manifest-reconcile.js +319 -62
- package/dist/manifest-reconcile.js.map +1 -1
- package/dist/manifest-reconcile.test.js +824 -2
- package/dist/manifest-reconcile.test.js.map +1 -1
- package/dist/personal-vault.d.ts +12 -0
- package/dist/personal-vault.d.ts.map +1 -1
- package/dist/personal-vault.js +20 -0
- package/dist/personal-vault.js.map +1 -1
- package/dist/qmd-reindex.d.ts +88 -11
- package/dist/qmd-reindex.d.ts.map +1 -1
- package/dist/qmd-reindex.js +354 -65
- package/dist/qmd-reindex.js.map +1 -1
- package/dist/qmd-reindex.test.d.ts +4 -0
- package/dist/qmd-reindex.test.d.ts.map +1 -1
- package/dist/qmd-reindex.test.js +285 -3
- package/dist/qmd-reindex.test.js.map +1 -1
- package/dist/s3.d.ts +13 -0
- package/dist/s3.d.ts.map +1 -1
- package/dist/s3.js +34 -1
- package/dist/s3.js.map +1 -1
- package/dist/s3.test.js +60 -1
- package/dist/s3.test.js.map +1 -1
- package/dist/telemetry.d.ts +22 -0
- package/dist/telemetry.d.ts.map +1 -1
- package/dist/telemetry.js +79 -0
- package/dist/telemetry.js.map +1 -1
- package/dist/telemetry.test.js +117 -1
- package/dist/telemetry.test.js.map +1 -1
- package/dist/watcher.d.ts +26 -1
- package/dist/watcher.d.ts.map +1 -1
- package/dist/watcher.js +75 -12
- package/dist/watcher.js.map +1 -1
- package/package.json +1 -1
- package/pnpm-workspace.yaml +1 -1
- package/src/active-company.test.ts +188 -0
- package/src/active-company.ts +168 -0
- package/src/bin/sync-runner-company.ts +29 -1
- package/src/bin/sync-runner-planning.test.ts +131 -0
- package/src/bin/sync-runner-planning.ts +60 -7
- package/src/bin/sync-runner-watch-loop.ts +322 -32
- package/src/bin/sync-runner-watch-routes.ts +14 -1
- package/src/bin/sync-runner.test.ts +1021 -16
- package/src/bin/sync-runner.ts +179 -28
- package/src/cli/rescue-core.ts +26 -1
- package/src/cli/rescue-drop-dir-symlink.test.ts +224 -0
- package/src/cli/share.test.ts +150 -6
- package/src/cli/share.ts +143 -48
- package/src/cli/sync.test.ts +150 -1
- package/src/cli/sync.ts +25 -29
- package/src/journal.test.ts +45 -0
- package/src/journal.ts +18 -1
- package/src/manifest-reconcile.test.ts +1019 -3
- package/src/manifest-reconcile.ts +424 -66
- package/src/personal-vault.ts +23 -0
- package/src/qmd-reindex.test.ts +324 -4
- package/src/qmd-reindex.ts +414 -76
- package/src/s3.test.ts +79 -0
- package/src/s3.ts +48 -1
- package/src/telemetry.test.ts +128 -0
- package/src/telemetry.ts +80 -0
- package/src/watcher.ts +111 -14
- package/test/e2e/watcher-real-chokidar.test.ts +62 -2
- package/test/e2e/watcher-recursive-backend.test.ts +67 -1
- package/test/joiner-manifest-reconcile.integration.test.ts +283 -0
package/src/cli/sync.test.ts
CHANGED
|
@@ -84,7 +84,10 @@ const mockVendResponse = {
|
|
|
84
84
|
};
|
|
85
85
|
|
|
86
86
|
function setupFetchMock(
|
|
87
|
-
opts: {
|
|
87
|
+
opts: {
|
|
88
|
+
tombstones?: Array<{ key: string; deletedAt: string }>;
|
|
89
|
+
tombstoneFailure?: boolean;
|
|
90
|
+
} = {},
|
|
88
91
|
) {
|
|
89
92
|
const fetchMock = vi.fn().mockImplementation(async (url: string) => {
|
|
90
93
|
const urlStr = String(url);
|
|
@@ -92,6 +95,7 @@ function setupFetchMock(
|
|
|
92
95
|
// every legacy test exercises the no-suppression path; tests that need
|
|
93
96
|
// suppression pass `{ tombstones }`.
|
|
94
97
|
if (urlStr.includes("/v1/files/tombstones")) {
|
|
98
|
+
if (opts.tombstoneFailure) throw new Error("tombstone lookup unavailable");
|
|
95
99
|
return {
|
|
96
100
|
ok: true,
|
|
97
101
|
status: 200,
|
|
@@ -1085,6 +1089,53 @@ describe("sync", () => {
|
|
|
1085
1089
|
expect(journal.files[key]?.removedAt).toBeUndefined();
|
|
1086
1090
|
});
|
|
1087
1091
|
|
|
1092
|
+
it("tombstone lookup failure does not resurrect a current intent-backed delete", async () => {
|
|
1093
|
+
const key = "docs/watcher-deleted.md";
|
|
1094
|
+
const localPath = path.join(tmpDir, "companies", "acme", key);
|
|
1095
|
+
vi.mocked(s3Module.listRemoteFiles).mockResolvedValueOnce([
|
|
1096
|
+
{ key, size: 42, lastModified: new Date(), etag: '"cur123"' },
|
|
1097
|
+
]);
|
|
1098
|
+
setupFetchMock({ tombstoneFailure: true });
|
|
1099
|
+
fs.writeFileSync(
|
|
1100
|
+
journalPath,
|
|
1101
|
+
JSON.stringify({
|
|
1102
|
+
version: "2",
|
|
1103
|
+
lastSync: new Date().toISOString(),
|
|
1104
|
+
files: {
|
|
1105
|
+
[key]: {
|
|
1106
|
+
hash: "synced-hash",
|
|
1107
|
+
size: 42,
|
|
1108
|
+
syncedAt: new Date().toISOString(),
|
|
1109
|
+
direction: "down",
|
|
1110
|
+
kind: "file",
|
|
1111
|
+
remoteEtag: "cur123",
|
|
1112
|
+
localDeleteIntent: {
|
|
1113
|
+
version: 1,
|
|
1114
|
+
remoteEtag: "cur123",
|
|
1115
|
+
localHash: "synced-hash",
|
|
1116
|
+
localKind: "file",
|
|
1117
|
+
},
|
|
1118
|
+
},
|
|
1119
|
+
},
|
|
1120
|
+
pulls: [],
|
|
1121
|
+
}),
|
|
1122
|
+
);
|
|
1123
|
+
|
|
1124
|
+
await sync({ company: "acme", vaultConfig: mockConfig, hqRoot: tmpDir });
|
|
1125
|
+
|
|
1126
|
+
expect(s3Module.downloadFile).not.toHaveBeenCalledWith(
|
|
1127
|
+
expect.anything(),
|
|
1128
|
+
key,
|
|
1129
|
+
expect.anything(),
|
|
1130
|
+
expect.anything(),
|
|
1131
|
+
);
|
|
1132
|
+
expect(fs.existsSync(localPath)).toBe(false);
|
|
1133
|
+
expect(
|
|
1134
|
+
JSON.parse(fs.readFileSync(journalPath, "utf8")).files[key]
|
|
1135
|
+
?.removedReason,
|
|
1136
|
+
).toBe("local-delete");
|
|
1137
|
+
});
|
|
1138
|
+
|
|
1088
1139
|
it("pull producer: a BULK disappearance is drift-restored, NOT tombstoned (ridge-incident guard in the pull leg)", async () => {
|
|
1089
1140
|
// 12 clean files vanish at once (corrupt mirror / bulk rm / checkout). The
|
|
1090
1141
|
// bulk-asymmetry guard refuses to treat this as intent — every file is
|
|
@@ -3480,6 +3531,104 @@ describe("sync", () => {
|
|
|
3480
3531
|
});
|
|
3481
3532
|
});
|
|
3482
3533
|
|
|
3534
|
+
it("converges after reindex prunes a downloaded generated core mirror (DEV-1974)", async () => {
|
|
3535
|
+
// Legacy personal-vault objects may be generated links below core/<type>.
|
|
3536
|
+
// A pull materializes the link, then reindex removes it because its
|
|
3537
|
+
// canonical source is elsewhere. The second pull must recognize the
|
|
3538
|
+
// journaled symlink as generated state rather than downloading it again.
|
|
3539
|
+
const linkKey = "core/policies/legacy-mirror.md";
|
|
3540
|
+
const linkPath = path.join(tmpDir, linkKey);
|
|
3541
|
+
const linkTarget = "../../personal/policies/legacy-mirror.md";
|
|
3542
|
+
const remote = {
|
|
3543
|
+
key: linkKey,
|
|
3544
|
+
size: 48,
|
|
3545
|
+
lastModified: new Date(),
|
|
3546
|
+
etag: '"legacy-mirror"',
|
|
3547
|
+
};
|
|
3548
|
+
|
|
3549
|
+
vi.mocked(s3Module.listRemoteFiles).mockResolvedValue([remote]);
|
|
3550
|
+
vi.mocked(s3Module.downloadFile).mockImplementation(
|
|
3551
|
+
async (_ctx: unknown, _key: string, localPath: string) => {
|
|
3552
|
+
fs.mkdirSync(path.dirname(localPath), { recursive: true });
|
|
3553
|
+
fs.symlinkSync(linkTarget, localPath);
|
|
3554
|
+
return { metadata: {} };
|
|
3555
|
+
},
|
|
3556
|
+
);
|
|
3557
|
+
|
|
3558
|
+
const first = await sync({
|
|
3559
|
+
company: "acme",
|
|
3560
|
+
vaultConfig: mockConfig,
|
|
3561
|
+
hqRoot: tmpDir,
|
|
3562
|
+
personalMode: true,
|
|
3563
|
+
});
|
|
3564
|
+
expect(first.filesDownloaded).toBe(1);
|
|
3565
|
+
expect(fs.lstatSync(linkPath).isSymbolicLink()).toBe(true);
|
|
3566
|
+
|
|
3567
|
+
// The real post-sync reindex removes this retired mirror. The unit test
|
|
3568
|
+
// mocks reindex globally, so make that lifecycle transition explicit.
|
|
3569
|
+
fs.unlinkSync(linkPath);
|
|
3570
|
+
|
|
3571
|
+
const second = await sync({
|
|
3572
|
+
company: "acme",
|
|
3573
|
+
vaultConfig: mockConfig,
|
|
3574
|
+
hqRoot: tmpDir,
|
|
3575
|
+
personalMode: true,
|
|
3576
|
+
});
|
|
3577
|
+
|
|
3578
|
+
expect(second.filesDownloaded).toBe(0);
|
|
3579
|
+
expect(vi.mocked(s3Module.downloadFile)).toHaveBeenCalledTimes(1);
|
|
3580
|
+
});
|
|
3581
|
+
|
|
3582
|
+
it("pulls a changed canonical object after a generated core mirror journal entry", async () => {
|
|
3583
|
+
const key = "core/policies/legacy-mirror.md";
|
|
3584
|
+
const localPath = path.join(tmpDir, key);
|
|
3585
|
+
const legacyMirror = {
|
|
3586
|
+
key,
|
|
3587
|
+
size: 48,
|
|
3588
|
+
lastModified: new Date(),
|
|
3589
|
+
etag: '"legacy-mirror"',
|
|
3590
|
+
};
|
|
3591
|
+
const canonicalReplacement = {
|
|
3592
|
+
...legacyMirror,
|
|
3593
|
+
lastModified: new Date(Date.now() + 1_000),
|
|
3594
|
+
etag: '"canonical-replacement"',
|
|
3595
|
+
};
|
|
3596
|
+
|
|
3597
|
+
vi.mocked(s3Module.listRemoteFiles)
|
|
3598
|
+
.mockResolvedValueOnce([legacyMirror])
|
|
3599
|
+
.mockResolvedValueOnce([canonicalReplacement]);
|
|
3600
|
+
vi.mocked(s3Module.downloadFile)
|
|
3601
|
+
.mockImplementationOnce(async (_ctx: unknown, _key: string, destination: string) => {
|
|
3602
|
+
fs.mkdirSync(path.dirname(destination), { recursive: true });
|
|
3603
|
+
fs.symlinkSync("../../personal/policies/legacy-mirror.md", destination);
|
|
3604
|
+
return { metadata: {} };
|
|
3605
|
+
})
|
|
3606
|
+
.mockImplementationOnce(async (_ctx: unknown, _key: string, destination: string) => {
|
|
3607
|
+
fs.mkdirSync(path.dirname(destination), { recursive: true });
|
|
3608
|
+
fs.writeFileSync(destination, "canonical replacement\n");
|
|
3609
|
+
return { metadata: {} };
|
|
3610
|
+
});
|
|
3611
|
+
|
|
3612
|
+
await sync({
|
|
3613
|
+
company: "acme",
|
|
3614
|
+
vaultConfig: mockConfig,
|
|
3615
|
+
hqRoot: tmpDir,
|
|
3616
|
+
personalMode: true,
|
|
3617
|
+
});
|
|
3618
|
+
fs.unlinkSync(localPath);
|
|
3619
|
+
|
|
3620
|
+
const second = await sync({
|
|
3621
|
+
company: "acme",
|
|
3622
|
+
vaultConfig: mockConfig,
|
|
3623
|
+
hqRoot: tmpDir,
|
|
3624
|
+
personalMode: true,
|
|
3625
|
+
});
|
|
3626
|
+
|
|
3627
|
+
expect(second.filesDownloaded).toBe(1);
|
|
3628
|
+
expect(fs.readFileSync(localPath, "utf8")).toBe("canonical replacement\n");
|
|
3629
|
+
expect(vi.mocked(s3Module.downloadFile)).toHaveBeenCalledTimes(2);
|
|
3630
|
+
});
|
|
3631
|
+
|
|
3483
3632
|
it("does not crash when local is a real directory at a remote-key path (EISDIR guard)", async () => {
|
|
3484
3633
|
// Codex round-9 P2 follow-up: pre-fix, the pull planner's
|
|
3485
3634
|
// post-lstat branch always called hashFile (readFileSync) on a
|
package/src/cli/sync.ts
CHANGED
|
@@ -48,7 +48,10 @@ import {
|
|
|
48
48
|
PERSONAL_VAULT_JOURNAL_SLUG,
|
|
49
49
|
migratePersonalVaultJournal,
|
|
50
50
|
} from "../journal.js";
|
|
51
|
-
import {
|
|
51
|
+
import {
|
|
52
|
+
isGeneratedCoreMirrorKey,
|
|
53
|
+
PERSONAL_VAULT_MANIFEST_KEY,
|
|
54
|
+
} from "../personal-vault.js";
|
|
52
55
|
import { isPersonalVaultExcluded } from "../personal-vault-exclusions.js";
|
|
53
56
|
import {
|
|
54
57
|
buildScopeShrinkPlan,
|
|
@@ -2122,33 +2125,6 @@ interface PullPlan {
|
|
|
2122
2125
|
const PULL_INTENTIONAL_DELETE_MIN_ABS = 10;
|
|
2123
2126
|
const PULL_INTENTIONAL_DELETE_RATIO = 0.1;
|
|
2124
2127
|
|
|
2125
|
-
// The four release-shipped `core/<type>/` namespaces the personal-overlay
|
|
2126
|
-
// mirror (reindex) materializes into. A stale mirror symlink pushed to the
|
|
2127
|
-
// vault lands as a single object at exactly `core/<type>/<...>/<entry>`, and if
|
|
2128
|
-
// a later release ships a REAL directory of the same name, pull sees a
|
|
2129
|
-
// (local real dir) vs (remote single object) collision at that key forever.
|
|
2130
|
-
const OVERLAY_MIRROR_CORE_TYPES = ["knowledge", "policies", "workers", "settings"];
|
|
2131
|
-
|
|
2132
|
-
/**
|
|
2133
|
-
* True when `key` is under a release-shipped personal-overlay-mirror core
|
|
2134
|
-
* namespace (`core/{knowledge,policies,workers,settings}/<entry>/...`).
|
|
2135
|
-
*
|
|
2136
|
-
* A remote LIST entry is always a single S3 object. A REAL release-shipped core
|
|
2137
|
-
* directory only ever syncs as child objects UNDER its key
|
|
2138
|
-
* (`core/knowledge/public/agent-browser/INDEX.md`), never as one object AT the
|
|
2139
|
-
* directory's own key — so a single object at such a key, colliding with a
|
|
2140
|
-
* local real directory, is unambiguously a stale overlay symlink marker, not
|
|
2141
|
-
* live core content. Pure path test — no I/O — safe inside the sync planner.
|
|
2142
|
-
*/
|
|
2143
|
-
function isOverlayMirrorCoreKey(key: string): boolean {
|
|
2144
|
-
const parts = key.split("/");
|
|
2145
|
-
return (
|
|
2146
|
-
parts.length >= 3 &&
|
|
2147
|
-
parts[0] === "core" &&
|
|
2148
|
-
OVERLAY_MIRROR_CORE_TYPES.includes(parts[1])
|
|
2149
|
-
);
|
|
2150
|
-
}
|
|
2151
|
-
|
|
2152
2128
|
function computePullPlan(
|
|
2153
2129
|
remoteFiles: RemoteFile[],
|
|
2154
2130
|
journal: SyncJournal,
|
|
@@ -2305,6 +2281,26 @@ function computePullPlan(
|
|
|
2305
2281
|
|
|
2306
2282
|
const journalEntry = getEntry(journal, remoteFile.key);
|
|
2307
2283
|
|
|
2284
|
+
// Reindex and package wiring own symlinks under these `core/<type>`
|
|
2285
|
+
// surfaces; their canonical sources are elsewhere. A prior pull records
|
|
2286
|
+
// the remote object as a symlink, then reindex removes the generated link.
|
|
2287
|
+
// Without this classification the next pull sees only an absent local path
|
|
2288
|
+
// and downloads the same marker forever. Retain regular core files: the
|
|
2289
|
+
// journal kind is the discriminator, because LIST exposes no object type.
|
|
2290
|
+
// An unchanged ETag is also required: a remote writer can replace a legacy
|
|
2291
|
+
// symlink with canonical file content at the same key, and that replacement
|
|
2292
|
+
// must still be pulled.
|
|
2293
|
+
if (
|
|
2294
|
+
personalMode &&
|
|
2295
|
+
journalEntry?.kind === "symlink" &&
|
|
2296
|
+
isGeneratedCoreMirrorKey(remoteFile.key) &&
|
|
2297
|
+
journalEntry.remoteEtag !== undefined &&
|
|
2298
|
+
!hasRemoteChanged(remoteFile, journalEntry)
|
|
2299
|
+
) {
|
|
2300
|
+
items.push({ action: "skip-excluded-policy", remoteFile, localPath });
|
|
2301
|
+
continue;
|
|
2302
|
+
}
|
|
2303
|
+
|
|
2308
2304
|
// ── FILE_TOMBSTONE consult (delete-resync) ───────────────────────────────
|
|
2309
2305
|
// A remote object present in the LIST may be an intentionally-deleted key
|
|
2310
2306
|
// (a peer re-pushed it, or its delete-marker hasn't propagated to this
|
|
@@ -2414,7 +2410,7 @@ function computePullPlan(
|
|
|
2414
2410
|
// "rm -rf the local directory" advice below is actively dangerous here
|
|
2415
2411
|
// (it would destroy release-shipped core content to pull inert derived
|
|
2416
2412
|
// state), and it recurred on every sync. feedback_ce9aeede.
|
|
2417
|
-
if (
|
|
2413
|
+
if (isGeneratedCoreMirrorKey(remoteFile.key)) {
|
|
2418
2414
|
console.error(
|
|
2419
2415
|
` Note: ${remoteFile.key} is a stale personal-overlay marker ` +
|
|
2420
2416
|
`shadowing a release-shipped core directory; keeping the local ` +
|
package/src/journal.test.ts
CHANGED
|
@@ -29,6 +29,7 @@ import {
|
|
|
29
29
|
PERSONAL_VAULT_JOURNAL_SLUG,
|
|
30
30
|
migratePersonalVaultJournal,
|
|
31
31
|
listJournals,
|
|
32
|
+
markLocalDeleteIntent,
|
|
32
33
|
} from "./journal.js";
|
|
33
34
|
import type { SyncJournal, PullRecord } from "./types.js";
|
|
34
35
|
|
|
@@ -745,6 +746,50 @@ describe("journal", () => {
|
|
|
745
746
|
// successful sync because it read one path keyed off the HQ-ROOT, while the
|
|
746
747
|
// engine writes slug-sharded journals (personal vault + per-company).
|
|
747
748
|
// `listJournals` is the all-shard enumeration the status surface now uses.
|
|
749
|
+
it("binds a local delete intent to the captured remote revision", () => {
|
|
750
|
+
const journal: SyncJournal = {
|
|
751
|
+
version: "2",
|
|
752
|
+
lastSync: "",
|
|
753
|
+
pulls: [],
|
|
754
|
+
files: {
|
|
755
|
+
"docs/a.md": {
|
|
756
|
+
hash: "local-hash",
|
|
757
|
+
size: 1,
|
|
758
|
+
syncedAt: new Date().toISOString(),
|
|
759
|
+
direction: "down",
|
|
760
|
+
kind: "file",
|
|
761
|
+
remoteEtag: "new-etag",
|
|
762
|
+
},
|
|
763
|
+
},
|
|
764
|
+
};
|
|
765
|
+
|
|
766
|
+
expect(
|
|
767
|
+
markLocalDeleteIntent(
|
|
768
|
+
journal,
|
|
769
|
+
"docs/a.md",
|
|
770
|
+
"local-hash",
|
|
771
|
+
"file",
|
|
772
|
+
"old-etag",
|
|
773
|
+
),
|
|
774
|
+
).toBe(false);
|
|
775
|
+
expect(journal.files["docs/a.md"]?.localDeleteIntent).toBeUndefined();
|
|
776
|
+
|
|
777
|
+
expect(
|
|
778
|
+
markLocalDeleteIntent(
|
|
779
|
+
journal,
|
|
780
|
+
"docs/a.md",
|
|
781
|
+
"local-hash",
|
|
782
|
+
"file",
|
|
783
|
+
"new-etag",
|
|
784
|
+
),
|
|
785
|
+
).toBe(true);
|
|
786
|
+
expect(journal.files["docs/a.md"]?.localDeleteIntent).toMatchObject({
|
|
787
|
+
remoteEtag: "new-etag",
|
|
788
|
+
localHash: "local-hash",
|
|
789
|
+
localKind: "file",
|
|
790
|
+
});
|
|
791
|
+
});
|
|
792
|
+
|
|
748
793
|
describe("listJournals", () => {
|
|
749
794
|
function seed(slug: string, files: SyncJournal["files"]): void {
|
|
750
795
|
writeJournal(slug, {
|
package/src/journal.ts
CHANGED
|
@@ -607,9 +607,15 @@ export function markLocalDeleteIntent(
|
|
|
607
607
|
relativePath: string,
|
|
608
608
|
localHash: string,
|
|
609
609
|
localKind: "file" | "symlink",
|
|
610
|
+
expectedRemoteEtag: string,
|
|
610
611
|
): boolean {
|
|
611
612
|
const entry = journal.files[relativePath];
|
|
612
|
-
if (
|
|
613
|
+
if (
|
|
614
|
+
!entry?.remoteEtag ||
|
|
615
|
+
entry.remoteEtag !== normalizeEtag(expectedRemoteEtag) ||
|
|
616
|
+
entry.hash !== localHash ||
|
|
617
|
+
entry.kind !== localKind
|
|
618
|
+
) {
|
|
613
619
|
return false;
|
|
614
620
|
}
|
|
615
621
|
entry.localDeleteIntent = {
|
|
@@ -621,6 +627,17 @@ export function markLocalDeleteIntent(
|
|
|
621
627
|
return true;
|
|
622
628
|
}
|
|
623
629
|
|
|
630
|
+
/** Invalidate a prior delete authorization after a watcher-observed recreate. */
|
|
631
|
+
export function clearLocalDeleteIntent(
|
|
632
|
+
journal: SyncJournal,
|
|
633
|
+
relativePath: string,
|
|
634
|
+
): boolean {
|
|
635
|
+
const entry = journal.files[relativePath];
|
|
636
|
+
if (!entry?.localDeleteIntent) return false;
|
|
637
|
+
delete entry.localDeleteIntent;
|
|
638
|
+
return true;
|
|
639
|
+
}
|
|
640
|
+
|
|
624
641
|
/** True if the entry is a tombstone (set by `tombstoneEntry`). */
|
|
625
642
|
export function isTombstone(entry: JournalEntry | undefined): boolean {
|
|
626
643
|
return !!entry && typeof entry.removedAt === "string";
|