@indigoai-us/hq-cloud 6.18.22-alpha.732d → 6.18.23
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/cli/sync.d.ts +6 -0
- package/dist/cli/sync.d.ts.map +1 -1
- package/dist/cli/sync.js +733 -613
- package/dist/cli/sync.js.map +1 -1
- package/dist/cli/sync.test.js +317 -0
- package/dist/cli/sync.test.js.map +1 -1
- package/dist/kms10-etag-safe-pull.d.ts +20 -0
- package/dist/kms10-etag-safe-pull.d.ts.map +1 -0
- package/dist/kms10-etag-safe-pull.js +37 -0
- package/dist/kms10-etag-safe-pull.js.map +1 -0
- package/dist/kms10-etag-safe-pull.test.d.ts +2 -0
- package/dist/kms10-etag-safe-pull.test.d.ts.map +1 -0
- package/dist/kms10-etag-safe-pull.test.js +53 -0
- package/dist/kms10-etag-safe-pull.test.js.map +1 -0
- package/package.json +1 -1
package/dist/cli/sync.test.js
CHANGED
|
@@ -3820,6 +3820,323 @@ describe("sync", () => {
|
|
|
3820
3820
|
expect(readJournal("acme").files["docs/handoff.md"]?.remoteEtag).toBe("new-etag");
|
|
3821
3821
|
expect(fs.readFileSync(localPath, "utf8")).toBe(localContent);
|
|
3822
3822
|
});
|
|
3823
|
+
it("keeps local edits without a conflict or GET when a marked KMS-10 copy matches the baseline", async () => {
|
|
3824
|
+
const companyDocs = path.join(tmpDir, "companies", "acme", "docs");
|
|
3825
|
+
fs.mkdirSync(companyDocs, { recursive: true });
|
|
3826
|
+
const localPath = path.join(companyDocs, "handoff.md");
|
|
3827
|
+
const baseline = "shared baseline";
|
|
3828
|
+
const localEdit = "local edit with a different byte length";
|
|
3829
|
+
fs.writeFileSync(localPath, localEdit);
|
|
3830
|
+
const stat = fs.lstatSync(localPath);
|
|
3831
|
+
const baselineHash = crypto.createHash("sha256").update(baseline).digest("hex");
|
|
3832
|
+
vi.mocked(s3Module.listRemoteFiles).mockResolvedValueOnce([
|
|
3833
|
+
{
|
|
3834
|
+
key: "docs/handoff.md",
|
|
3835
|
+
size: Buffer.byteLength(baseline),
|
|
3836
|
+
lastModified: new Date(),
|
|
3837
|
+
etag: '"kms10-copy-etag"',
|
|
3838
|
+
},
|
|
3839
|
+
]);
|
|
3840
|
+
vi.mocked(s3Module.headRemoteFile).mockResolvedValueOnce({
|
|
3841
|
+
etag: '"kms10-copy-etag"',
|
|
3842
|
+
size: Buffer.byteLength(baseline),
|
|
3843
|
+
lastModified: new Date(),
|
|
3844
|
+
metadata: {
|
|
3845
|
+
"hq-kms10-reencrypt-current": "true",
|
|
3846
|
+
"hq-kms10-original-key-sha256": crypto.createHash("sha256").update("docs/handoff.md", "utf8").digest("hex"),
|
|
3847
|
+
[s3Module.FILE_CONTENT_SHA256_META_KEY]: baselineHash,
|
|
3848
|
+
},
|
|
3849
|
+
});
|
|
3850
|
+
writeJournal("acme", {
|
|
3851
|
+
version: "2",
|
|
3852
|
+
lastSync: new Date().toISOString(),
|
|
3853
|
+
pulls: [],
|
|
3854
|
+
files: {
|
|
3855
|
+
"docs/handoff.md": {
|
|
3856
|
+
hash: baselineHash,
|
|
3857
|
+
size: Buffer.byteLength(baseline),
|
|
3858
|
+
mtimeMs: stat.mtimeMs - 1,
|
|
3859
|
+
ctimeMs: stat.ctimeMs - 1,
|
|
3860
|
+
remoteEtag: "old-kms-etag",
|
|
3861
|
+
syncedAt: new Date(Date.now() - 3600000).toISOString(),
|
|
3862
|
+
direction: "down",
|
|
3863
|
+
kind: "file",
|
|
3864
|
+
},
|
|
3865
|
+
},
|
|
3866
|
+
});
|
|
3867
|
+
const result = await sync({
|
|
3868
|
+
company: "acme",
|
|
3869
|
+
onConflict: "keep",
|
|
3870
|
+
vaultConfig: mockConfig,
|
|
3871
|
+
hqRoot: tmpDir,
|
|
3872
|
+
pullPrefixes: ["docs/handoff.md"],
|
|
3873
|
+
skipReindex: true,
|
|
3874
|
+
});
|
|
3875
|
+
expect(vi.mocked(s3Module.headRemoteFile)).toHaveBeenCalledTimes(1);
|
|
3876
|
+
expect(result.conflicts).toBe(0);
|
|
3877
|
+
expect(vi.mocked(s3Module.downloadFile)).not.toHaveBeenCalled();
|
|
3878
|
+
expect(fs.readFileSync(localPath, "utf8")).toBe(localEdit);
|
|
3879
|
+
expect(readJournal("acme").files["docs/handoff.md"]).toMatchObject({
|
|
3880
|
+
hash: baselineHash,
|
|
3881
|
+
remoteEtag: "kms10-copy-etag",
|
|
3882
|
+
});
|
|
3883
|
+
});
|
|
3884
|
+
it("HEADs changed same-size candidates in bounded concurrent batches", async () => {
|
|
3885
|
+
const companyDocs = path.join(tmpDir, "companies", "acme", "docs");
|
|
3886
|
+
fs.mkdirSync(companyDocs, { recursive: true });
|
|
3887
|
+
const rows = [];
|
|
3888
|
+
const journalFiles = {};
|
|
3889
|
+
const heads = new Map();
|
|
3890
|
+
for (let index = 0; index < 17; index += 1) {
|
|
3891
|
+
const key = `docs/batch-${String(index).padStart(2, "0")}.md`;
|
|
3892
|
+
const localPath = path.join(tmpDir, "companies", "acme", key);
|
|
3893
|
+
const content = `baseline-${String(index).padStart(2, "0")}`;
|
|
3894
|
+
fs.writeFileSync(localPath, content);
|
|
3895
|
+
const stat = fs.lstatSync(localPath);
|
|
3896
|
+
const hash = crypto.createHash("sha256").update(content).digest("hex");
|
|
3897
|
+
const oldEtag = `old-etag-${index}`;
|
|
3898
|
+
const newEtag = `new-etag-${index}`;
|
|
3899
|
+
rows.push({ key, size: stat.size, lastModified: new Date(), etag: `"${newEtag}"` });
|
|
3900
|
+
journalFiles[key] = {
|
|
3901
|
+
hash,
|
|
3902
|
+
size: stat.size,
|
|
3903
|
+
mtimeMs: stat.mtimeMs,
|
|
3904
|
+
ctimeMs: stat.ctimeMs,
|
|
3905
|
+
remoteEtag: oldEtag,
|
|
3906
|
+
syncedAt: new Date(Date.now() - 3_600_000).toISOString(),
|
|
3907
|
+
direction: "down",
|
|
3908
|
+
kind: "file",
|
|
3909
|
+
};
|
|
3910
|
+
heads.set(key, {
|
|
3911
|
+
etag: `"${newEtag}"`,
|
|
3912
|
+
size: stat.size,
|
|
3913
|
+
lastModified: new Date(),
|
|
3914
|
+
metadata: {
|
|
3915
|
+
"hq-kms10-reencrypt-current": "true",
|
|
3916
|
+
"hq-kms10-original-key-sha256": crypto.createHash("sha256").update(key, "utf8").digest("hex"),
|
|
3917
|
+
"hq-kms10-original-etag": oldEtag,
|
|
3918
|
+
},
|
|
3919
|
+
});
|
|
3920
|
+
}
|
|
3921
|
+
writeJournal("acme", {
|
|
3922
|
+
version: "2",
|
|
3923
|
+
lastSync: new Date().toISOString(),
|
|
3924
|
+
pulls: [],
|
|
3925
|
+
files: journalFiles,
|
|
3926
|
+
});
|
|
3927
|
+
vi.mocked(s3Module.listRemoteFiles).mockResolvedValueOnce(rows);
|
|
3928
|
+
let active = 0;
|
|
3929
|
+
let maximumActive = 0;
|
|
3930
|
+
vi.mocked(s3Module.headRemoteFile).mockImplementation(async (_ctx, key) => {
|
|
3931
|
+
active += 1;
|
|
3932
|
+
maximumActive = Math.max(maximumActive, active);
|
|
3933
|
+
// Hold each fake network operation long enough to observe overlap.
|
|
3934
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
3935
|
+
active -= 1;
|
|
3936
|
+
return heads.get(key) ?? null;
|
|
3937
|
+
});
|
|
3938
|
+
const result = await sync({
|
|
3939
|
+
company: "acme",
|
|
3940
|
+
onConflict: "keep",
|
|
3941
|
+
vaultConfig: mockConfig,
|
|
3942
|
+
hqRoot: tmpDir,
|
|
3943
|
+
pullPrefixes: ["docs/"],
|
|
3944
|
+
skipReindex: true,
|
|
3945
|
+
});
|
|
3946
|
+
expect(result.conflicts).toBe(0);
|
|
3947
|
+
expect(result.filesDownloaded).toBe(0);
|
|
3948
|
+
expect(maximumActive).toBeGreaterThan(1);
|
|
3949
|
+
expect(maximumActive).toBeLessThanOrEqual(16);
|
|
3950
|
+
expect(maximumActive).toBe(16);
|
|
3951
|
+
});
|
|
3952
|
+
it("records a marked KMS-10 ETag when the baseline bytes match but local stat changed", async () => {
|
|
3953
|
+
const companyDocs = path.join(tmpDir, "companies", "acme", "docs");
|
|
3954
|
+
fs.mkdirSync(companyDocs, { recursive: true });
|
|
3955
|
+
const localPath = path.join(companyDocs, "handoff.md");
|
|
3956
|
+
const baseline = "same content after a stat-only change";
|
|
3957
|
+
fs.writeFileSync(localPath, baseline);
|
|
3958
|
+
const stat = fs.lstatSync(localPath);
|
|
3959
|
+
const baselineHash = crypto.createHash("sha256").update(baseline).digest("hex");
|
|
3960
|
+
vi.mocked(s3Module.listRemoteFiles).mockResolvedValueOnce([{
|
|
3961
|
+
key: "docs/handoff.md",
|
|
3962
|
+
size: Buffer.byteLength(baseline),
|
|
3963
|
+
lastModified: new Date(),
|
|
3964
|
+
etag: '"kms10-copy-etag"',
|
|
3965
|
+
}]);
|
|
3966
|
+
vi.mocked(s3Module.headRemoteFile).mockResolvedValueOnce({
|
|
3967
|
+
etag: '"kms10-copy-etag"',
|
|
3968
|
+
size: Buffer.byteLength(baseline),
|
|
3969
|
+
lastModified: new Date(),
|
|
3970
|
+
metadata: {
|
|
3971
|
+
"hq-kms10-reencrypt-current": "true",
|
|
3972
|
+
"hq-kms10-original-key-sha256": crypto.createHash("sha256").update("docs/handoff.md", "utf8").digest("hex"),
|
|
3973
|
+
[s3Module.FILE_CONTENT_SHA256_META_KEY]: baselineHash,
|
|
3974
|
+
},
|
|
3975
|
+
});
|
|
3976
|
+
writeJournal("acme", {
|
|
3977
|
+
version: "2",
|
|
3978
|
+
lastSync: new Date().toISOString(),
|
|
3979
|
+
pulls: [],
|
|
3980
|
+
files: {
|
|
3981
|
+
"docs/handoff.md": {
|
|
3982
|
+
hash: baselineHash,
|
|
3983
|
+
size: Buffer.byteLength(baseline),
|
|
3984
|
+
mtimeMs: stat.mtimeMs - 1,
|
|
3985
|
+
ctimeMs: stat.ctimeMs - 1,
|
|
3986
|
+
remoteEtag: "old-kms-etag",
|
|
3987
|
+
syncedAt: new Date(Date.now() - 3600000).toISOString(),
|
|
3988
|
+
direction: "down",
|
|
3989
|
+
kind: "file",
|
|
3990
|
+
},
|
|
3991
|
+
},
|
|
3992
|
+
});
|
|
3993
|
+
const result = await sync({
|
|
3994
|
+
company: "acme",
|
|
3995
|
+
onConflict: "keep",
|
|
3996
|
+
vaultConfig: mockConfig,
|
|
3997
|
+
hqRoot: tmpDir,
|
|
3998
|
+
pullPrefixes: ["docs/handoff.md"],
|
|
3999
|
+
skipReindex: true,
|
|
4000
|
+
});
|
|
4001
|
+
expect(result.conflicts).toBe(0);
|
|
4002
|
+
expect(vi.mocked(s3Module.downloadFile)).not.toHaveBeenCalled();
|
|
4003
|
+
expect(fs.readFileSync(localPath, "utf8")).toBe(baseline);
|
|
4004
|
+
expect(readJournal("acme").files["docs/handoff.md"]?.remoteEtag).toBe("kms10-copy-etag");
|
|
4005
|
+
});
|
|
4006
|
+
it.each([
|
|
4007
|
+
{ label: "unchanged", localTarget: "baseline-target.md" },
|
|
4008
|
+
{ label: "locally retargeted", localTarget: "new-local-target.md" },
|
|
4009
|
+
])("records a marked KMS-10 ETag for an $label symlink without a GET or conflict", async ({ localTarget }) => {
|
|
4010
|
+
const companyDocs = path.join(tmpDir, "companies", "acme", "docs");
|
|
4011
|
+
fs.mkdirSync(companyDocs, { recursive: true });
|
|
4012
|
+
const localPath = path.join(companyDocs, "link.md");
|
|
4013
|
+
const baselineTarget = "baseline-target.md";
|
|
4014
|
+
fs.symlinkSync(localTarget, localPath);
|
|
4015
|
+
const stat = fs.lstatSync(localPath);
|
|
4016
|
+
const baselineHash = journalModule.hashSymlinkTarget(baselineTarget);
|
|
4017
|
+
// The remote body is the encoded link, so LIST reports its byte length
|
|
4018
|
+
// while the journal records a symlink with size 0.
|
|
4019
|
+
const remoteSize = Buffer.byteLength(`${s3Module.SYMLINK_BODY_PREFIX}${baselineTarget}`);
|
|
4020
|
+
vi.mocked(s3Module.listRemoteFiles).mockResolvedValueOnce([{
|
|
4021
|
+
key: "docs/link.md",
|
|
4022
|
+
size: remoteSize,
|
|
4023
|
+
lastModified: new Date(),
|
|
4024
|
+
etag: '"kms10-copy-etag"',
|
|
4025
|
+
}]);
|
|
4026
|
+
vi.mocked(s3Module.headRemoteFile).mockResolvedValueOnce({
|
|
4027
|
+
etag: '"kms10-copy-etag"',
|
|
4028
|
+
size: remoteSize,
|
|
4029
|
+
lastModified: new Date(),
|
|
4030
|
+
metadata: {
|
|
4031
|
+
"hq-kms10-reencrypt-current": "true",
|
|
4032
|
+
"hq-kms10-original-key-sha256": crypto.createHash("sha256").update("docs/link.md", "utf8").digest("hex"),
|
|
4033
|
+
"hq-kms10-original-etag": "old-kms-etag",
|
|
4034
|
+
},
|
|
4035
|
+
});
|
|
4036
|
+
writeJournal("acme", {
|
|
4037
|
+
version: "2",
|
|
4038
|
+
lastSync: new Date().toISOString(),
|
|
4039
|
+
pulls: [],
|
|
4040
|
+
files: {
|
|
4041
|
+
"docs/link.md": {
|
|
4042
|
+
hash: baselineHash,
|
|
4043
|
+
size: 0,
|
|
4044
|
+
mtimeMs: stat.mtimeMs,
|
|
4045
|
+
ctimeMs: stat.ctimeMs,
|
|
4046
|
+
remoteEtag: "old-kms-etag",
|
|
4047
|
+
syncedAt: new Date(Date.now() - 3600000).toISOString(),
|
|
4048
|
+
direction: "down",
|
|
4049
|
+
kind: "symlink",
|
|
4050
|
+
},
|
|
4051
|
+
},
|
|
4052
|
+
});
|
|
4053
|
+
const result = await sync({
|
|
4054
|
+
company: "acme",
|
|
4055
|
+
onConflict: "keep",
|
|
4056
|
+
vaultConfig: mockConfig,
|
|
4057
|
+
hqRoot: tmpDir,
|
|
4058
|
+
pullPrefixes: ["docs/link.md"],
|
|
4059
|
+
skipReindex: true,
|
|
4060
|
+
});
|
|
4061
|
+
expect(vi.mocked(s3Module.headRemoteFile)).toHaveBeenCalledTimes(1);
|
|
4062
|
+
expect(result.conflicts).toBe(0);
|
|
4063
|
+
expect(vi.mocked(s3Module.downloadFile)).not.toHaveBeenCalled();
|
|
4064
|
+
expect(fs.readlinkSync(localPath)).toBe(localTarget);
|
|
4065
|
+
expect(readJournal("acme").files["docs/link.md"]).toMatchObject({
|
|
4066
|
+
hash: baselineHash,
|
|
4067
|
+
remoteEtag: "kms10-copy-etag",
|
|
4068
|
+
});
|
|
4069
|
+
});
|
|
4070
|
+
it("leaves the journal unchanged and marks a retry when local bytes change after KMS-10 planning", async () => {
|
|
4071
|
+
const companyDocs = path.join(tmpDir, "companies", "acme", "docs");
|
|
4072
|
+
fs.mkdirSync(companyDocs, { recursive: true });
|
|
4073
|
+
const localPath = path.join(companyDocs, "handoff.md");
|
|
4074
|
+
const baseline = "shared baseline";
|
|
4075
|
+
const localEdit = "local edit that differs from baseline";
|
|
4076
|
+
fs.writeFileSync(localPath, localEdit);
|
|
4077
|
+
const stat = fs.lstatSync(localPath);
|
|
4078
|
+
const baselineHash = crypto.createHash("sha256").update(baseline).digest("hex");
|
|
4079
|
+
vi.mocked(s3Module.listRemoteFiles).mockResolvedValueOnce([{
|
|
4080
|
+
key: "docs/handoff.md",
|
|
4081
|
+
size: Buffer.byteLength(baseline),
|
|
4082
|
+
lastModified: new Date(),
|
|
4083
|
+
etag: '"kms10-copy-etag"',
|
|
4084
|
+
}]);
|
|
4085
|
+
vi.mocked(s3Module.headRemoteFile).mockResolvedValueOnce({
|
|
4086
|
+
etag: '"kms10-copy-etag"',
|
|
4087
|
+
size: Buffer.byteLength(baseline),
|
|
4088
|
+
lastModified: new Date(),
|
|
4089
|
+
metadata: {
|
|
4090
|
+
"hq-kms10-reencrypt-current": "true",
|
|
4091
|
+
"hq-kms10-original-key-sha256": crypto.createHash("sha256").update("docs/handoff.md", "utf8").digest("hex"),
|
|
4092
|
+
[s3Module.FILE_CONTENT_SHA256_META_KEY]: baselineHash,
|
|
4093
|
+
},
|
|
4094
|
+
});
|
|
4095
|
+
writeJournal("acme", {
|
|
4096
|
+
version: "2",
|
|
4097
|
+
lastSync: new Date().toISOString(),
|
|
4098
|
+
pulls: [],
|
|
4099
|
+
files: {
|
|
4100
|
+
"docs/handoff.md": {
|
|
4101
|
+
hash: baselineHash,
|
|
4102
|
+
size: Buffer.byteLength(baseline),
|
|
4103
|
+
mtimeMs: stat.mtimeMs - 1,
|
|
4104
|
+
ctimeMs: stat.ctimeMs - 1,
|
|
4105
|
+
remoteEtag: "old-kms-etag",
|
|
4106
|
+
syncedAt: new Date(Date.now() - 3600000).toISOString(),
|
|
4107
|
+
direction: "down",
|
|
4108
|
+
kind: "file",
|
|
4109
|
+
},
|
|
4110
|
+
},
|
|
4111
|
+
});
|
|
4112
|
+
const events = [];
|
|
4113
|
+
const checkpointApply = vi.fn();
|
|
4114
|
+
const result = await sync({
|
|
4115
|
+
company: "acme",
|
|
4116
|
+
onConflict: "keep",
|
|
4117
|
+
vaultConfig: mockConfig,
|
|
4118
|
+
hqRoot: tmpDir,
|
|
4119
|
+
pullPrefixes: ["docs/handoff.md"],
|
|
4120
|
+
skipReindex: true,
|
|
4121
|
+
reconcileApply: { store: { checkpointApply }, cursor: {} },
|
|
4122
|
+
onEvent: (event) => {
|
|
4123
|
+
events.push(event);
|
|
4124
|
+
if (event.type === "plan")
|
|
4125
|
+
fs.writeFileSync(localPath, "concurrent edit after planning");
|
|
4126
|
+
},
|
|
4127
|
+
});
|
|
4128
|
+
expect(result.conflicts).toBe(0);
|
|
4129
|
+
expect(vi.mocked(s3Module.downloadFile)).not.toHaveBeenCalled();
|
|
4130
|
+
expect(fs.readFileSync(localPath, "utf8")).toBe("concurrent edit after planning");
|
|
4131
|
+
expect(readJournal("acme").files["docs/handoff.md"]?.remoteEtag).toBe("old-kms-etag");
|
|
4132
|
+
expect(checkpointApply).not.toHaveBeenCalled();
|
|
4133
|
+
expect(events).toContainEqual(expect.objectContaining({
|
|
4134
|
+
type: "error",
|
|
4135
|
+
diagnostic: true,
|
|
4136
|
+
path: "docs/handoff.md",
|
|
4137
|
+
message: expect.stringContaining("local file changed after planning"),
|
|
4138
|
+
}));
|
|
4139
|
+
});
|
|
3823
4140
|
it.each([
|
|
3824
4141
|
"changed remote content hash",
|
|
3825
4142
|
"locally modified file",
|