@indigoai-us/hq-cloud 5.4.5 → 5.7.0
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/dist/bin/sync-runner.d.ts +13 -0
- package/dist/bin/sync-runner.d.ts.map +1 -1
- package/dist/bin/sync-runner.js +14 -2
- package/dist/bin/sync-runner.js.map +1 -1
- package/dist/bin/sync-runner.test.js +37 -0
- package/dist/bin/sync-runner.test.js.map +1 -1
- package/dist/cli/share.d.ts.map +1 -1
- package/dist/cli/share.js +114 -24
- package/dist/cli/share.js.map +1 -1
- package/dist/cli/share.test.js +212 -7
- package/dist/cli/share.test.js.map +1 -1
- package/dist/cli/sync.d.ts +22 -0
- package/dist/cli/sync.d.ts.map +1 -1
- package/dist/cli/sync.js +174 -62
- package/dist/cli/sync.js.map +1 -1
- package/dist/cli/sync.test.js +126 -0
- package/dist/cli/sync.test.js.map +1 -1
- package/dist/journal.d.ts +7 -1
- package/dist/journal.d.ts.map +1 -1
- package/dist/journal.js +16 -2
- package/dist/journal.js.map +1 -1
- package/dist/s3.d.ts +3 -1
- package/dist/s3.d.ts.map +1 -1
- package/dist/s3.js +2 -1
- package/dist/s3.js.map +1 -1
- package/dist/types.d.ts +8 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/watcher.js +2 -2
- package/dist/watcher.js.map +1 -1
- package/package.json +1 -1
- package/src/bin/sync-runner.test.ts +43 -0
- package/src/bin/sync-runner.ts +25 -2
- package/src/cli/share.test.ts +257 -7
- package/src/cli/share.ts +169 -26
- package/src/cli/sync.test.ts +151 -0
- package/src/cli/sync.ts +256 -67
- package/src/journal.ts +16 -1
- package/src/s3.ts +4 -2
- package/src/types.ts +8 -0
- package/src/watcher.ts +2 -2
package/dist/s3.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"s3.js","sourceRoot":"","sources":["../src/s3.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,MAAM,IAAI,CAAC;AACzB,OAAO,KAAK,IAAI,MAAM,MAAM,CAAC;AAC7B,OAAO,EACL,QAAQ,EACR,gBAAgB,EAChB,gBAAgB,EAChB,oBAAoB,EACpB,mBAAmB,EACnB,iBAAiB,GAClB,MAAM,oBAAoB,CAAC;AAG5B;;;;GAIG;AACH,SAAS,WAAW,CAAC,GAAkB;IACrC,OAAO,IAAI,QAAQ,CAAC;QAClB,MAAM,EAAE,GAAG,CAAC,MAAM;QAClB,WAAW,EAAE;YACX,WAAW,EAAE,GAAG,CAAC,WAAW,CAAC,WAAW;YACxC,eAAe,EAAE,GAAG,CAAC,WAAW,CAAC,eAAe;YAChD,YAAY,EAAE,GAAG,CAAC,WAAW,CAAC,YAAY;SAC3C;KACF,CAAC,CAAC;AACL,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,UAAU,CAC9B,GAAkB,EAClB,SAAiB,EACjB,GAAW;IAEX,MAAM,MAAM,GAAG,WAAW,CAAC,GAAG,CAAC,CAAC;IAChC,MAAM,IAAI,GAAG,EAAE,CAAC,YAAY,CAAC,SAAS,CAAC,CAAC;IAExC,MAAM,MAAM,CAAC,IAAI,
|
|
1
|
+
{"version":3,"file":"s3.js","sourceRoot":"","sources":["../src/s3.ts"],"names":[],"mappings":"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"}
|
package/dist/types.d.ts
CHANGED
|
@@ -23,6 +23,14 @@ export interface JournalEntry {
|
|
|
23
23
|
size: number;
|
|
24
24
|
syncedAt: string;
|
|
25
25
|
direction: "up" | "down";
|
|
26
|
+
/**
|
|
27
|
+
* S3 ETag of the remote object as of last successful sync, normalized (no
|
|
28
|
+
* surrounding quotes). Optional for backwards compatibility: entries
|
|
29
|
+
* written before this field existed won't have it, in which case
|
|
30
|
+
* conflict detection falls back to comparing remote `lastModified`
|
|
31
|
+
* against `syncedAt`.
|
|
32
|
+
*/
|
|
33
|
+
remoteEtag?: string;
|
|
26
34
|
}
|
|
27
35
|
export interface SyncJournal {
|
|
28
36
|
version: "1";
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,MAAM,WAAW,UAAU;IACzB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,WAAW;IAC1B,WAAW,EAAE,MAAM,CAAC;IACpB,eAAe,EAAE,MAAM,CAAC;IACxB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,IAAI,GAAG,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,MAAM,WAAW,UAAU;IACzB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,WAAW;IAC1B,WAAW,EAAE,MAAM,CAAC;IACpB,eAAe,EAAE,MAAM,CAAC;IACxB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,IAAI,GAAG,MAAM,CAAC;IACzB;;;;;;OAMG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,GAAG,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;CACrC;AAED,MAAM,WAAW,UAAU;IACzB,OAAO,EAAE,OAAO,CAAC;IACjB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;AAED,MAAM,WAAW,UAAU;IACzB,aAAa,EAAE,MAAM,CAAC;IACtB,aAAa,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,UAAU;IACzB,eAAe,EAAE,MAAM,CAAC;IACxB,eAAe,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,WAAW;IAC1B,GAAG,EAAE,MAAM,CAAC;IACZ,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED;;;GAGG;AACH,MAAM,WAAW,aAAa;IAC5B,yBAAyB;IACzB,GAAG,EAAE,MAAM,CAAC;IACZ,4EAA4E;IAC5E,IAAI,EAAE,MAAM,CAAC;IACb,qCAAqC;IACrC,UAAU,EAAE,MAAM,CAAC;IACnB,iBAAiB;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,6BAA6B;IAC7B,WAAW,EAAE,gBAAgB,CAAC;IAC9B,6CAA6C;IAC7C,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,gBAAgB;IAC/B,WAAW,EAAE,MAAM,CAAC;IACpB,eAAe,EAAE,MAAM,CAAC;IACxB,YAAY,EAAE,MAAM,CAAC;CACtB;AAED;;GAEG;AACH,MAAM,WAAW,kBAAkB;IACjC,8DAA8D;IAC9D,MAAM,EAAE,MAAM,CAAC;IACf,2CAA2C;IAC3C,SAAS,EAAE,MAAM,CAAC;IAClB,wEAAwE;IACxE,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB"}
|
package/dist/watcher.js
CHANGED
|
@@ -91,8 +91,8 @@ export class SyncWatcher {
|
|
|
91
91
|
const existing = journal.files[relativePath];
|
|
92
92
|
if (existing && existing.hash === hash)
|
|
93
93
|
continue;
|
|
94
|
-
await uploadFile(this.ctx, change.absolutePath, relativePath);
|
|
95
|
-
updateEntry(journal, relativePath, hash, stat.size, "up");
|
|
94
|
+
const { etag } = await uploadFile(this.ctx, change.absolutePath, relativePath);
|
|
95
|
+
updateEntry(journal, relativePath, hash, stat.size, "up", etag);
|
|
96
96
|
}
|
|
97
97
|
}
|
|
98
98
|
catch (err) {
|
package/dist/watcher.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"watcher.js","sourceRoot":"","sources":["../src/watcher.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,MAAM,IAAI,CAAC;AACzB,OAAO,KAAK,IAAI,MAAM,MAAM,CAAC;AAC7B,OAAO,EAAE,KAAK,EAAE,MAAM,UAAU,CAAC;AAGjC,OAAO,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AACpE,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,QAAQ,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAChF,OAAO,EAAE,UAAU,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAC;AAEvD,MAAM,WAAW,GAAG,IAAI,CAAC;AAQzB,MAAM,OAAO,WAAW;IACd,OAAO,GAAqB,IAAI,CAAC;IACjC,MAAM,CAAS;IACf,GAAG,CAAgB;IACnB,UAAU,CAAgC;IAC1C,cAAc,GAAG,IAAI,GAAG,EAAyB,CAAC;IAClD,aAAa,GAAyC,IAAI,CAAC;IAC3D,UAAU,GAAG,KAAK,CAAC;IAE3B,YAAY,MAAc,EAAE,GAAkB;QAC5C,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;QACrB,IAAI,CAAC,GAAG,GAAG,GAAG,CAAC;QACf,IAAI,CAAC,UAAU,GAAG,kBAAkB,CAAC,MAAM,CAAC,CAAC;IAC/C,CAAC;IAED,KAAK;QACH,IAAI,IAAI,CAAC,OAAO;YAAE,OAAO;QAEzB,IAAI,CAAC,OAAO,GAAG,KAAK,CAAC,IAAI,CAAC,MAAM,EAAE;YAChC,OAAO,EAAE,CAAC,QAAgB,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC;YACzD,UAAU,EAAE,IAAI;YAChB,aAAa,EAAE,IAAI;YACnB,gBAAgB,EAAE;gBAChB,kBAAkB,EAAE,GAAG;gBACvB,YAAY,EAAE,GAAG;aAClB;SACF,CAAC,CAAC;QAEH,IAAI,CAAC,OAAO;aACT,EAAE,CAAC,KAAK,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,WAAW,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC;aAC5C,EAAE,CAAC,QAAQ,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,WAAW,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC;aAClD,EAAE,CAAC,QAAQ,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,WAAW,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC;aAClD,EAAE,CAAC,OAAO,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC,gBAAgB,EAAE,GAAG,CAAC,CAAC,CAAC;IAChE,CAAC;IAED,IAAI;QACF,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC;YACjB,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,CAAC;YACrB,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC;QACtB,CAAC;QACD,IAAI,IAAI,CAAC,aAAa,EAAE,CAAC;YACvB,YAAY,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC;YACjC,IAAI,CAAC,aAAa,GAAG,IAAI,CAAC;QAC5B,CAAC;IACH,CAAC;IAEO,WAAW,CAAC,IAAiC,EAAE,YAAoB;QACzE,MAAM,YAAY,GAAG,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;QAE9D,oCAAoC;QACpC,IAAI,IAAI,KAAK,QAAQ,IAAI,CAAC,iBAAiB,CAAC,YAAY,CAAC,EAAE,CAAC;YAC1D,OAAO;QACT,CAAC;QAED,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC,YAAY,EAAE;YACpC,IAAI;YACJ,YAAY;YACZ,YAAY;SACb,CAAC,CAAC;QAEH,4DAA4D;QAC5D,IAAI,IAAI,CAAC,aAAa,EAAE,CAAC;YACvB,YAAY,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC;QACnC,CAAC;QACD,IAAI,CAAC,aAAa,GAAG,UAAU,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,KAAK,EAAE,EAAE,WAAW,CAAC,CAAC;IACnE,CAAC;IAEO,KAAK,CAAC,KAAK;QACjB,IAAI,IAAI,CAAC,UAAU,IAAI,IAAI,CAAC,cAAc,CAAC,IAAI,KAAK,CAAC;YAAE,OAAO;QAC9D,IAAI,CAAC,UAAU,GAAG,IAAI,CAAC;QAEvB,MAAM,KAAK,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC;QAC3C,IAAI,CAAC,cAAc,CAAC,KAAK,EAAE,CAAC;QAE5B,MAAM,OAAO,GAAG,WAAW,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QAE3C,KAAK,MAAM,CAAC,YAAY,EAAE,MAAM,CAAC,IAAI,KAAK,EAAE,CAAC;YAC3C,IAAI,CAAC;gBACH,IAAI,MAAM,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;oBAC7B,MAAM,gBAAgB,CAAC,IAAI,CAAC,GAAG,EAAE,YAAY,CAAC,CAAC;oBAC/C,OAAO,OAAO,CAAC,KAAK,CAAC,YAAY,CAAC,CAAC;gBACrC,CAAC;qBAAM,CAAC;oBACN,MAAM,IAAI,GAAG,QAAQ,CAAC,MAAM,CAAC,YAAY,CAAC,CAAC;oBAC3C,MAAM,IAAI,GAAG,EAAE,CAAC,QAAQ,CAAC,MAAM,CAAC,YAAY,CAAC,CAAC;oBAE9C,mCAAmC;oBACnC,MAAM,QAAQ,GAAG,OAAO,CAAC,KAAK,CAAC,YAAY,CAAC,CAAC;oBAC7C,IAAI,QAAQ,IAAI,QAAQ,CAAC,IAAI,KAAK,IAAI;wBAAE,SAAS;oBAEjD,MAAM,UAAU,CAAC,IAAI,CAAC,GAAG,EAAE,MAAM,CAAC,YAAY,EAAE,YAAY,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"watcher.js","sourceRoot":"","sources":["../src/watcher.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -575,6 +575,49 @@ describe("per-company fanout", () => {
|
|
|
575
575
|
]);
|
|
576
576
|
});
|
|
577
577
|
|
|
578
|
+
it("tags Stage-1 plan events with the company slug", async () => {
|
|
579
|
+
const deps = makeDeps({
|
|
580
|
+
createVaultClient: () =>
|
|
581
|
+
makeVaultStub({
|
|
582
|
+
memberships: [{ companyUid: "cmp_a" }],
|
|
583
|
+
entityGet: (uid: string) =>
|
|
584
|
+
Promise.resolve({ uid, slug: "acme" } as unknown as EntityInfo),
|
|
585
|
+
}),
|
|
586
|
+
sync: vi.fn().mockImplementation(async (opts: SyncOptions) => {
|
|
587
|
+
opts.onEvent?.({
|
|
588
|
+
type: "plan",
|
|
589
|
+
filesToDownload: 7,
|
|
590
|
+
bytesToDownload: 4096,
|
|
591
|
+
filesToUpload: 0,
|
|
592
|
+
bytesToUpload: 0,
|
|
593
|
+
filesToSkip: 3,
|
|
594
|
+
filesToConflict: 1,
|
|
595
|
+
});
|
|
596
|
+
return defaultSyncResult({ filesDownloaded: 7, bytesDownloaded: 4096 });
|
|
597
|
+
}),
|
|
598
|
+
});
|
|
599
|
+
|
|
600
|
+
const code = await runRunner(["--companies"], deps);
|
|
601
|
+
expect(code).toBe(0);
|
|
602
|
+
const planEvents = deps.stdout
|
|
603
|
+
.events()
|
|
604
|
+
.filter((e): e is Extract<RunnerEvent, { type: "plan" }> =>
|
|
605
|
+
e.type === "plan",
|
|
606
|
+
);
|
|
607
|
+
expect(planEvents).toEqual([
|
|
608
|
+
{
|
|
609
|
+
type: "plan",
|
|
610
|
+
company: "acme",
|
|
611
|
+
filesToDownload: 7,
|
|
612
|
+
bytesToDownload: 4096,
|
|
613
|
+
filesToUpload: 0,
|
|
614
|
+
bytesToUpload: 0,
|
|
615
|
+
filesToSkip: 3,
|
|
616
|
+
filesToConflict: 1,
|
|
617
|
+
},
|
|
618
|
+
]);
|
|
619
|
+
});
|
|
620
|
+
|
|
578
621
|
it("tags per-file error events with the company slug", async () => {
|
|
579
622
|
const deps = makeDeps({
|
|
580
623
|
createVaultClient: () =>
|
package/src/bin/sync-runner.ts
CHANGED
|
@@ -125,6 +125,18 @@ export type RunnerEvent =
|
|
|
125
125
|
type: "fanout-plan";
|
|
126
126
|
companies: Array<{ uid: string; slug: string; name?: string }>;
|
|
127
127
|
}
|
|
128
|
+
| ({
|
|
129
|
+
/**
|
|
130
|
+
* Stage-1 results for a single company's sync/share pass. Emitted once
|
|
131
|
+
* before any `progress` events for that company arrive — once for the
|
|
132
|
+
* pull phase (download counts) and once for the push phase (upload
|
|
133
|
+
* counts) when `--direction both`. Consumers (the menubar) sum the
|
|
134
|
+
* non-zero fields across all `plan` events seen for a fanout to render
|
|
135
|
+
* an accurate "X of Y files" denominator before transfers begin.
|
|
136
|
+
*/
|
|
137
|
+
type: "plan";
|
|
138
|
+
company: string;
|
|
139
|
+
} & Omit<Extract<SyncProgressEvent, { type: "plan" }>, "type">)
|
|
128
140
|
| ({ type: "progress"; company: string } & Omit<Extract<SyncProgressEvent, { type: "progress" }>, "type">)
|
|
129
141
|
| ({ type: "error"; company?: string } & Omit<Extract<SyncProgressEvent, { type: "error" }>, "type">)
|
|
130
142
|
| ({ type: "conflict"; company: string } & Omit<Extract<SyncProgressEvent, { type: "conflict" }>, "type">)
|
|
@@ -534,7 +546,18 @@ export async function runRunner(
|
|
|
534
546
|
// Per-company event tagger — shared by push and pull phases so progress
|
|
535
547
|
// rows land on the right company regardless of which phase emitted them.
|
|
536
548
|
const tagAndEmit = (event: SyncProgressEvent): void => {
|
|
537
|
-
if (event.type === "
|
|
549
|
+
if (event.type === "plan") {
|
|
550
|
+
emit({
|
|
551
|
+
type: "plan",
|
|
552
|
+
company: companyLabel,
|
|
553
|
+
filesToDownload: event.filesToDownload,
|
|
554
|
+
bytesToDownload: event.bytesToDownload,
|
|
555
|
+
filesToUpload: event.filesToUpload,
|
|
556
|
+
bytesToUpload: event.bytesToUpload,
|
|
557
|
+
filesToSkip: event.filesToSkip,
|
|
558
|
+
filesToConflict: event.filesToConflict,
|
|
559
|
+
});
|
|
560
|
+
} else if (event.type === "progress") {
|
|
538
561
|
emit({
|
|
539
562
|
type: "progress",
|
|
540
563
|
company: companyLabel,
|
|
@@ -550,7 +573,7 @@ export async function runRunner(
|
|
|
550
573
|
direction: event.direction,
|
|
551
574
|
resolution: event.resolution,
|
|
552
575
|
});
|
|
553
|
-
} else {
|
|
576
|
+
} else if (event.type === "error") {
|
|
554
577
|
emit({
|
|
555
578
|
type: "error",
|
|
556
579
|
company: companyLabel,
|
package/src/cli/share.test.ts
CHANGED
|
@@ -9,9 +9,11 @@ import * as os from "os";
|
|
|
9
9
|
import { clearContextCache } from "../context.js";
|
|
10
10
|
import type { VaultServiceConfig } from "../types.js";
|
|
11
11
|
|
|
12
|
-
// Mock s3 module at the top level
|
|
12
|
+
// Mock s3 module at the top level. uploadFile resolves to a synthetic ETag
|
|
13
|
+
// so share() can record it on the journal entry — the real PutObject
|
|
14
|
+
// response shape is `{ ETag: '"<hex>"' }`.
|
|
13
15
|
vi.mock("../s3.js", () => ({
|
|
14
|
-
uploadFile: vi.fn().mockResolvedValue(
|
|
16
|
+
uploadFile: vi.fn().mockResolvedValue({ etag: '"upload-etag"' }),
|
|
15
17
|
downloadFile: vi.fn().mockResolvedValue(undefined),
|
|
16
18
|
listRemoteFiles: vi.fn().mockResolvedValue([]),
|
|
17
19
|
deleteRemoteFile: vi.fn().mockResolvedValue(undefined),
|
|
@@ -77,6 +79,10 @@ describe("share", () => {
|
|
|
77
79
|
afterEach(() => {
|
|
78
80
|
vi.unstubAllGlobals();
|
|
79
81
|
vi.clearAllMocks();
|
|
82
|
+
// clearAllMocks wipes the default ETag impl set in vi.mock(), so
|
|
83
|
+
// re-prime it for the next test.
|
|
84
|
+
vi.mocked(uploadFile).mockResolvedValue({ etag: '"upload-etag"' });
|
|
85
|
+
vi.mocked(headRemoteFile).mockResolvedValue(null);
|
|
80
86
|
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
81
87
|
fs.rmSync(stateDir, { recursive: true, force: true });
|
|
82
88
|
delete process.env.HQ_STATE_DIR;
|
|
@@ -276,18 +282,18 @@ describe("share", () => {
|
|
|
276
282
|
expect(uploadFile).toHaveBeenCalledWith(expect.anything(), testFile, "changed.md");
|
|
277
283
|
});
|
|
278
284
|
|
|
279
|
-
it("populates conflictPaths and emits a conflict event when remote drifted from journal", async () => {
|
|
285
|
+
it("populates conflictPaths and emits a conflict event when both local and remote drifted from journal", async () => {
|
|
280
286
|
const companyRoot = path.join(tmpDir, "companies", "acme");
|
|
281
287
|
fs.mkdirSync(companyRoot, { recursive: true });
|
|
282
288
|
const testFile = path.join(companyRoot, "drifted.md");
|
|
283
289
|
fs.writeFileSync(testFile, "local edit");
|
|
284
290
|
|
|
285
|
-
//
|
|
286
|
-
//
|
|
287
|
-
//
|
|
291
|
+
// Stale hash → local diverged. Remote ETag in head response differs
|
|
292
|
+
// from the one stored in the journal → remote also moved. Both sides
|
|
293
|
+
// changed since last sync = real conflict.
|
|
288
294
|
vi.mocked(headRemoteFile).mockResolvedValueOnce({
|
|
289
295
|
lastModified: new Date(),
|
|
290
|
-
etag: '"remote-
|
|
296
|
+
etag: '"remote-new-etag"',
|
|
291
297
|
size: 99,
|
|
292
298
|
});
|
|
293
299
|
|
|
@@ -303,6 +309,7 @@ describe("share", () => {
|
|
|
303
309
|
size: 10,
|
|
304
310
|
syncedAt: new Date().toISOString(),
|
|
305
311
|
direction: "up",
|
|
312
|
+
remoteEtag: "remote-old-etag",
|
|
306
313
|
},
|
|
307
314
|
},
|
|
308
315
|
}),
|
|
@@ -332,6 +339,124 @@ describe("share", () => {
|
|
|
332
339
|
});
|
|
333
340
|
});
|
|
334
341
|
|
|
342
|
+
it("uploads (no conflict) when only the local side changed since last sync", async () => {
|
|
343
|
+
// Regression for hq-cloud#<conflict-detection>: a local edit to a file
|
|
344
|
+
// that exists on S3 used to trigger a push conflict because the
|
|
345
|
+
// detector compared `journalEntry.hash !== localHash` without checking
|
|
346
|
+
// the remote. Combined with `--on-conflict keep`, this silently dropped
|
|
347
|
+
// every edit to any pre-existing file.
|
|
348
|
+
const companyRoot = path.join(tmpDir, "companies", "acme");
|
|
349
|
+
fs.mkdirSync(companyRoot, { recursive: true });
|
|
350
|
+
const testFile = path.join(companyRoot, "edited.md");
|
|
351
|
+
fs.writeFileSync(testFile, "edited locally");
|
|
352
|
+
|
|
353
|
+
const syncedAt = new Date(Date.now() - 60_000).toISOString();
|
|
354
|
+
vi.mocked(headRemoteFile).mockResolvedValueOnce({
|
|
355
|
+
lastModified: new Date(Date.parse(syncedAt) - 30_000),
|
|
356
|
+
etag: '"unchanged-remote"',
|
|
357
|
+
size: 5,
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
const journalPath = path.join(stateDir, "sync-journal.acme.json");
|
|
361
|
+
fs.writeFileSync(
|
|
362
|
+
journalPath,
|
|
363
|
+
JSON.stringify({
|
|
364
|
+
version: "1",
|
|
365
|
+
lastSync: syncedAt,
|
|
366
|
+
files: {
|
|
367
|
+
"edited.md": {
|
|
368
|
+
hash: "stale-hash-for-old-content",
|
|
369
|
+
size: 5,
|
|
370
|
+
syncedAt,
|
|
371
|
+
direction: "down",
|
|
372
|
+
remoteEtag: "unchanged-remote",
|
|
373
|
+
},
|
|
374
|
+
},
|
|
375
|
+
}),
|
|
376
|
+
);
|
|
377
|
+
|
|
378
|
+
const events: unknown[] = [];
|
|
379
|
+
const result = await share({
|
|
380
|
+
paths: [testFile],
|
|
381
|
+
company: "acme",
|
|
382
|
+
vaultConfig: mockConfig,
|
|
383
|
+
hqRoot: tmpDir,
|
|
384
|
+
onConflict: "keep",
|
|
385
|
+
onEvent: (e) => events.push(e),
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
expect(result.conflictPaths).toEqual([]);
|
|
389
|
+
expect(result.filesUploaded).toBe(1);
|
|
390
|
+
expect(events.some((e): e is { type: string } =>
|
|
391
|
+
typeof e === "object" && e !== null && (e as { type?: string }).type === "conflict",
|
|
392
|
+
)).toBe(false);
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
it("falls back to lastModified vs syncedAt when journal entry has no remoteEtag (legacy)", async () => {
|
|
396
|
+
// Legacy entries from before the remoteEtag field existed should be
|
|
397
|
+
// treated as "remote unchanged" iff lastModified <= syncedAt.
|
|
398
|
+
const companyRoot = path.join(tmpDir, "companies", "acme");
|
|
399
|
+
fs.mkdirSync(companyRoot, { recursive: true });
|
|
400
|
+
const testFile = path.join(companyRoot, "legacy.md");
|
|
401
|
+
fs.writeFileSync(testFile, "edited locally");
|
|
402
|
+
|
|
403
|
+
const syncedAt = new Date().toISOString();
|
|
404
|
+
vi.mocked(headRemoteFile).mockResolvedValueOnce({
|
|
405
|
+
lastModified: new Date(Date.parse(syncedAt) - 5_000),
|
|
406
|
+
etag: '"some-etag"',
|
|
407
|
+
size: 5,
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
const journalPath = path.join(stateDir, "sync-journal.acme.json");
|
|
411
|
+
fs.writeFileSync(
|
|
412
|
+
journalPath,
|
|
413
|
+
JSON.stringify({
|
|
414
|
+
version: "1",
|
|
415
|
+
lastSync: syncedAt,
|
|
416
|
+
files: {
|
|
417
|
+
"legacy.md": {
|
|
418
|
+
hash: "stale-hash",
|
|
419
|
+
size: 5,
|
|
420
|
+
syncedAt,
|
|
421
|
+
direction: "down",
|
|
422
|
+
// no remoteEtag — pre-fix journal
|
|
423
|
+
},
|
|
424
|
+
},
|
|
425
|
+
}),
|
|
426
|
+
);
|
|
427
|
+
|
|
428
|
+
const result = await share({
|
|
429
|
+
paths: [testFile],
|
|
430
|
+
company: "acme",
|
|
431
|
+
vaultConfig: mockConfig,
|
|
432
|
+
hqRoot: tmpDir,
|
|
433
|
+
onConflict: "keep",
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
expect(result.conflictPaths).toEqual([]);
|
|
437
|
+
expect(result.filesUploaded).toBe(1);
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
it("records the upload's ETag on the journal entry", async () => {
|
|
441
|
+
const companyRoot = path.join(tmpDir, "companies", "acme");
|
|
442
|
+
fs.mkdirSync(companyRoot, { recursive: true });
|
|
443
|
+
const testFile = path.join(companyRoot, "fresh.md");
|
|
444
|
+
fs.writeFileSync(testFile, "new file");
|
|
445
|
+
|
|
446
|
+
vi.mocked(uploadFile).mockResolvedValueOnce({ etag: '"new-upload-etag"' });
|
|
447
|
+
|
|
448
|
+
await share({
|
|
449
|
+
paths: [testFile],
|
|
450
|
+
company: "acme",
|
|
451
|
+
vaultConfig: mockConfig,
|
|
452
|
+
hqRoot: tmpDir,
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
const journalPath = path.join(stateDir, "sync-journal.acme.json");
|
|
456
|
+
const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
|
|
457
|
+
expect(journal.files["fresh.md"].remoteEtag).toBe("new-upload-etag");
|
|
458
|
+
});
|
|
459
|
+
|
|
335
460
|
it("skipUnchanged=false (default) uploads even when hash matches", async () => {
|
|
336
461
|
const companyRoot = path.join(tmpDir, "companies", "acme");
|
|
337
462
|
fs.mkdirSync(companyRoot, { recursive: true });
|
|
@@ -383,6 +508,9 @@ describe("share", () => {
|
|
|
383
508
|
vaultConfig: mockConfig,
|
|
384
509
|
hqRoot: tmpDir,
|
|
385
510
|
onEvent: (e) => {
|
|
511
|
+
// Only file-level events carry `.path`. The Stage-1 `plan` event is
|
|
512
|
+
// surfaced separately and tested in its own block.
|
|
513
|
+
if (e.type === "plan") return;
|
|
386
514
|
events.push({
|
|
387
515
|
type: e.type,
|
|
388
516
|
path: e.path,
|
|
@@ -396,4 +524,126 @@ describe("share", () => {
|
|
|
396
524
|
expect(events.every((e) => e.type === "progress")).toBe(true);
|
|
397
525
|
expect(events.map((e) => e.path).sort()).toEqual(["a.md", "b.md"]);
|
|
398
526
|
});
|
|
527
|
+
|
|
528
|
+
// ── Stage-1 plan event ─────────────────────────────────────────────────
|
|
529
|
+
|
|
530
|
+
it("emits a plan event before any progress events", async () => {
|
|
531
|
+
const companyRoot = path.join(tmpDir, "companies", "acme");
|
|
532
|
+
fs.mkdirSync(companyRoot, { recursive: true });
|
|
533
|
+
fs.writeFileSync(path.join(companyRoot, "a.md"), "alpha");
|
|
534
|
+
fs.writeFileSync(path.join(companyRoot, "b.md"), "beta");
|
|
535
|
+
|
|
536
|
+
const events: { type: string }[] = [];
|
|
537
|
+
await share({
|
|
538
|
+
paths: [companyRoot],
|
|
539
|
+
company: "acme",
|
|
540
|
+
vaultConfig: mockConfig,
|
|
541
|
+
hqRoot: tmpDir,
|
|
542
|
+
onEvent: (e) => events.push({ type: e.type }),
|
|
543
|
+
});
|
|
544
|
+
|
|
545
|
+
expect(events.length).toBeGreaterThan(0);
|
|
546
|
+
expect(events[0].type).toBe("plan");
|
|
547
|
+
const planIndex = events.findIndex((e) => e.type === "plan");
|
|
548
|
+
const firstProgressIndex = events.findIndex((e) => e.type === "progress");
|
|
549
|
+
expect(firstProgressIndex).toBeGreaterThan(planIndex);
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
it("plan event reports filesToUpload = candidates and bytesToUpload = sum of file sizes", async () => {
|
|
553
|
+
const companyRoot = path.join(tmpDir, "companies", "acme");
|
|
554
|
+
fs.mkdirSync(companyRoot, { recursive: true });
|
|
555
|
+
fs.writeFileSync(path.join(companyRoot, "a.md"), "alpha"); // 5 bytes
|
|
556
|
+
fs.writeFileSync(path.join(companyRoot, "b.md"), "beta!"); // 5 bytes
|
|
557
|
+
|
|
558
|
+
const planEvents: Array<{
|
|
559
|
+
type: string;
|
|
560
|
+
filesToUpload?: number;
|
|
561
|
+
bytesToUpload?: number;
|
|
562
|
+
filesToDownload?: number;
|
|
563
|
+
bytesToDownload?: number;
|
|
564
|
+
filesToSkip?: number;
|
|
565
|
+
filesToConflict?: number;
|
|
566
|
+
}> = [];
|
|
567
|
+
await share({
|
|
568
|
+
paths: [companyRoot],
|
|
569
|
+
company: "acme",
|
|
570
|
+
vaultConfig: mockConfig,
|
|
571
|
+
hqRoot: tmpDir,
|
|
572
|
+
onEvent: (e) => {
|
|
573
|
+
if (e.type === "plan") planEvents.push(e);
|
|
574
|
+
},
|
|
575
|
+
});
|
|
576
|
+
|
|
577
|
+
expect(planEvents).toHaveLength(1);
|
|
578
|
+
expect(planEvents[0]).toMatchObject({
|
|
579
|
+
type: "plan",
|
|
580
|
+
filesToUpload: 2,
|
|
581
|
+
bytesToUpload: 10,
|
|
582
|
+
filesToDownload: 0, // share() is push-only
|
|
583
|
+
bytesToDownload: 0,
|
|
584
|
+
filesToSkip: 0,
|
|
585
|
+
// Push conflicts can't be classified pre-HEAD (V1 limitation);
|
|
586
|
+
// the complete event reports the authoritative count.
|
|
587
|
+
filesToConflict: 0,
|
|
588
|
+
});
|
|
589
|
+
});
|
|
590
|
+
|
|
591
|
+
it("plan event filesToSkip reflects skip-unchanged hits when journal hash matches", async () => {
|
|
592
|
+
const companyRoot = path.join(tmpDir, "companies", "acme");
|
|
593
|
+
fs.mkdirSync(companyRoot, { recursive: true });
|
|
594
|
+
fs.writeFileSync(path.join(companyRoot, "unchanged.md"), "stable content");
|
|
595
|
+
fs.writeFileSync(path.join(companyRoot, "changed.md"), "newer content");
|
|
596
|
+
|
|
597
|
+
// Pre-seed the journal so unchanged.md matches its hash but
|
|
598
|
+
// changed.md does not.
|
|
599
|
+
const crypto = await import("crypto");
|
|
600
|
+
const unchangedHash = crypto
|
|
601
|
+
.createHash("sha256")
|
|
602
|
+
.update("stable content")
|
|
603
|
+
.digest("hex");
|
|
604
|
+
const journalPath = path.join(stateDir, "sync-journal.acme.json");
|
|
605
|
+
fs.writeFileSync(
|
|
606
|
+
journalPath,
|
|
607
|
+
JSON.stringify({
|
|
608
|
+
version: "1",
|
|
609
|
+
lastSync: new Date().toISOString(),
|
|
610
|
+
files: {
|
|
611
|
+
"unchanged.md": {
|
|
612
|
+
hash: unchangedHash,
|
|
613
|
+
size: 14,
|
|
614
|
+
syncedAt: new Date().toISOString(),
|
|
615
|
+
direction: "up",
|
|
616
|
+
},
|
|
617
|
+
"changed.md": {
|
|
618
|
+
hash: "stale-hash",
|
|
619
|
+
size: 13,
|
|
620
|
+
syncedAt: new Date().toISOString(),
|
|
621
|
+
direction: "up",
|
|
622
|
+
},
|
|
623
|
+
},
|
|
624
|
+
}),
|
|
625
|
+
);
|
|
626
|
+
|
|
627
|
+
const planEvents: Array<{
|
|
628
|
+
type: string;
|
|
629
|
+
filesToUpload?: number;
|
|
630
|
+
filesToSkip?: number;
|
|
631
|
+
}> = [];
|
|
632
|
+
await share({
|
|
633
|
+
paths: [companyRoot],
|
|
634
|
+
company: "acme",
|
|
635
|
+
vaultConfig: mockConfig,
|
|
636
|
+
hqRoot: tmpDir,
|
|
637
|
+
skipUnchanged: true,
|
|
638
|
+
onEvent: (e) => {
|
|
639
|
+
if (e.type === "plan") planEvents.push(e);
|
|
640
|
+
},
|
|
641
|
+
});
|
|
642
|
+
|
|
643
|
+
expect(planEvents).toHaveLength(1);
|
|
644
|
+
expect(planEvents[0]).toMatchObject({
|
|
645
|
+
filesToUpload: 1,
|
|
646
|
+
filesToSkip: 1,
|
|
647
|
+
});
|
|
648
|
+
});
|
|
399
649
|
});
|