@indigoai-us/hq-cloud 6.15.78 → 6.15.80
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/area-ledger-migration.d.ts +13 -0
- package/dist/bin/area-ledger-migration.d.ts.map +1 -0
- package/dist/bin/area-ledger-migration.js +64 -0
- package/dist/bin/area-ledger-migration.js.map +1 -0
- package/dist/bin/area-ledger-migration.test.d.ts +2 -0
- package/dist/bin/area-ledger-migration.test.d.ts.map +1 -0
- package/dist/bin/area-ledger-migration.test.js +55 -0
- package/dist/bin/area-ledger-migration.test.js.map +1 -0
- package/dist/bin/sync-runner-company.d.ts +6 -0
- package/dist/bin/sync-runner-company.d.ts.map +1 -1
- package/dist/bin/sync-runner-company.js +6 -0
- package/dist/bin/sync-runner-company.js.map +1 -1
- package/dist/bin/sync-runner-company.test.js +47 -0
- package/dist/bin/sync-runner-company.test.js.map +1 -1
- package/dist/cli/share.d.ts +8 -0
- package/dist/cli/share.d.ts.map +1 -1
- package/dist/cli/share.js +184 -10
- package/dist/cli/share.js.map +1 -1
- package/dist/cli/share.test.js +330 -7
- package/dist/cli/share.test.js.map +1 -1
- package/dist/cli/sync.d.ts +7 -0
- package/dist/cli/sync.d.ts.map +1 -1
- package/dist/cli/sync.js +11 -1
- package/dist/cli/sync.js.map +1 -1
- package/dist/cli/sync.test.js +25 -0
- package/dist/cli/sync.test.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/index.test.js +11 -1
- package/dist/index.test.js.map +1 -1
- package/dist/journal-store.test.js +42 -1
- package/dist/journal-store.test.js.map +1 -1
- package/dist/journal.d.ts +13 -1
- package/dist/journal.d.ts.map +1 -1
- package/dist/journal.js +104 -3
- package/dist/journal.js.map +1 -1
- package/dist/sync/area-ledger-migration.d.ts +116 -0
- package/dist/sync/area-ledger-migration.d.ts.map +1 -0
- package/dist/sync/area-ledger-migration.js +505 -0
- package/dist/sync/area-ledger-migration.js.map +1 -0
- package/dist/sync/area-ledger-migration.test.d.ts +2 -0
- package/dist/sync/area-ledger-migration.test.d.ts.map +1 -0
- package/dist/sync/area-ledger-migration.test.js +274 -0
- package/dist/sync/area-ledger-migration.test.js.map +1 -0
- package/dist/sync/area-ledger.d.ts +93 -0
- package/dist/sync/area-ledger.d.ts.map +1 -0
- package/dist/sync/area-ledger.js +460 -0
- package/dist/sync/area-ledger.js.map +1 -0
- package/dist/sync/area-ledger.test.d.ts +2 -0
- package/dist/sync/area-ledger.test.d.ts.map +1 -0
- package/dist/sync/area-ledger.test.js +199 -0
- package/dist/sync/area-ledger.test.js.map +1 -0
- package/dist/sync/event-sync.d.ts +14 -4
- package/dist/sync/event-sync.d.ts.map +1 -1
- package/dist/sync/event-sync.js +177 -61
- package/dist/sync/event-sync.js.map +1 -1
- package/dist/sync/event-sync.test.js +140 -10
- package/dist/sync/event-sync.test.js.map +1 -1
- package/dist/sync/index.d.ts +2 -0
- package/dist/sync/index.d.ts.map +1 -1
- package/dist/sync/index.js +1 -0
- package/dist/sync/index.js.map +1 -1
- package/dist/sync/parallel-sync-coordinator.d.ts.map +1 -1
- package/dist/sync/parallel-sync-coordinator.js +22 -4
- package/dist/sync/parallel-sync-coordinator.js.map +1 -1
- package/dist/sync/parallel-sync-coordinator.test.js +45 -0
- package/dist/sync/parallel-sync-coordinator.test.js.map +1 -1
- package/dist/sync/reconcile-cursor.d.ts +97 -0
- package/dist/sync/reconcile-cursor.d.ts.map +1 -0
- package/dist/sync/reconcile-cursor.js +215 -0
- package/dist/sync/reconcile-cursor.js.map +1 -0
- package/dist/sync/reconcile-cursor.test.d.ts +2 -0
- package/dist/sync/reconcile-cursor.test.d.ts.map +1 -0
- package/dist/sync/reconcile-cursor.test.js +139 -0
- package/dist/sync/reconcile-cursor.test.js.map +1 -0
- package/dist/sync/state-store.d.ts +76 -0
- package/dist/sync/state-store.d.ts.map +1 -1
- package/dist/sync/state-store.js +317 -3
- package/dist/sync/state-store.js.map +1 -1
- package/dist/sync/state-store.test.js +39 -1
- package/dist/sync/state-store.test.js.map +1 -1
- package/dist/sync/sync-work.d.ts +2 -1
- package/dist/sync/sync-work.d.ts.map +1 -1
- package/dist/sync/sync-work.js.map +1 -1
- package/dist/types.d.ts +6 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/vault-client.d.ts +9 -2
- package/dist/vault-client.d.ts.map +1 -1
- package/dist/vault-client.js +3 -2
- package/dist/vault-client.js.map +1 -1
- package/dist/vault-client.test.js +26 -0
- package/dist/vault-client.test.js.map +1 -1
- package/package.json +3 -2
package/dist/cli/share.test.js
CHANGED
|
@@ -56,6 +56,7 @@ import { share, ServerOwnedPushPathsError, UnreachablePushPathsError, Unregister
|
|
|
56
56
|
import { deleteRemoteFile, downloadFile, headRemoteFile, primeObjectTransport, primeUploads, uploadFile, uploadSymlink, WindowsSymlinkPrivilegeError, } from "../s3.js";
|
|
57
57
|
import { VaultAuthError, } from "../vault-client.js";
|
|
58
58
|
import { hashFile, readJournal, writeJournal } from "../journal.js";
|
|
59
|
+
import { ReconcileCursorStore } from "../sync/reconcile-cursor.js";
|
|
59
60
|
/**
|
|
60
61
|
* Read a journal the way production does.
|
|
61
62
|
*
|
|
@@ -144,6 +145,8 @@ describe("company skill identity preflight", () => {
|
|
|
144
145
|
stateDir = fs.mkdtempSync(path.join(os.tmpdir(), "hq-share-skill-state-"));
|
|
145
146
|
process.env.HQ_STATE_DIR = stateDir;
|
|
146
147
|
vi.mocked(uploadFile).mockClear();
|
|
148
|
+
vi.mocked(headRemoteFile).mockReset();
|
|
149
|
+
vi.mocked(headRemoteFile).mockResolvedValue(null);
|
|
147
150
|
});
|
|
148
151
|
afterEach(() => {
|
|
149
152
|
fs.rmSync(hqRoot, { recursive: true, force: true });
|
|
@@ -157,13 +160,16 @@ describe("company skill identity preflight", () => {
|
|
|
157
160
|
return file;
|
|
158
161
|
}
|
|
159
162
|
function registered(input, uid = "skl_PREFLIGHT1") {
|
|
163
|
+
const stamped = parseRegisteredCompanySkillUid(input.content)
|
|
164
|
+
? input.content
|
|
165
|
+
: input.content.replace("---\n", `---\nskill_uid: ${uid}\n`);
|
|
160
166
|
return {
|
|
161
167
|
ok: true,
|
|
162
168
|
skillUid: uid,
|
|
163
169
|
path: input.path,
|
|
164
170
|
vaultPath: input.path,
|
|
165
|
-
content:
|
|
166
|
-
action: "created",
|
|
171
|
+
content: stamped,
|
|
172
|
+
action: parseRegisteredCompanySkillUid(input.content) ? "reused" : "created",
|
|
167
173
|
accessPolicy: "open",
|
|
168
174
|
skill: { name: "Call Prep", description: "Prepare calls", tags: [] },
|
|
169
175
|
};
|
|
@@ -177,7 +183,7 @@ describe("company skill identity preflight", () => {
|
|
|
177
183
|
entityContext: makeEntityContext(),
|
|
178
184
|
registerCompanySkill: registrar,
|
|
179
185
|
});
|
|
180
|
-
expect(registrar).
|
|
186
|
+
expect(registrar).toHaveBeenNthCalledWith(1, {
|
|
181
187
|
companySlug: "acme",
|
|
182
188
|
path: "skills/call-prep/SKILL.md",
|
|
183
189
|
content: expect.stringContaining("name: Call Prep"),
|
|
@@ -185,6 +191,12 @@ describe("company skill identity preflight", () => {
|
|
|
185
191
|
expect(parseRegisteredCompanySkillUid(fs.readFileSync(file, "utf8"))).toBe("skl_PREFLIGHT1");
|
|
186
192
|
expect(result.filesUploaded).toBe(1);
|
|
187
193
|
expect(uploadFile).toHaveBeenCalledWith(expect.anything(), file, "skills/call-prep/SKILL.md", undefined, expect.anything());
|
|
194
|
+
expect(registrar).toHaveBeenNthCalledWith(2, {
|
|
195
|
+
companySlug: "acme",
|
|
196
|
+
path: "skills/call-prep/SKILL.md",
|
|
197
|
+
content: expect.stringContaining("skill_uid: skl_PREFLIGHT1"),
|
|
198
|
+
landed: true,
|
|
199
|
+
});
|
|
188
200
|
});
|
|
189
201
|
it("fails closed without an authenticated registration seam", async () => {
|
|
190
202
|
const file = createSkill("manual", "---\nname: Manual\ndescription: Missing id\n---\n");
|
|
@@ -196,17 +208,276 @@ describe("company skill identity preflight", () => {
|
|
|
196
208
|
expect(uploadFile).not.toHaveBeenCalled();
|
|
197
209
|
expect(parseRegisteredCompanySkillUid(fs.readFileSync(file, "utf8"))).toBeUndefined();
|
|
198
210
|
});
|
|
199
|
-
it("
|
|
211
|
+
it("reconciles an already stamped canonical skill after upload", async () => {
|
|
200
212
|
const file = createSkill("existing", "---\nname: Existing\ndescription: Ready\nskill_uid: skl_EXISTING1\n---\n");
|
|
201
|
-
const registrar = vi.fn();
|
|
213
|
+
const registrar = vi.fn(async (input) => registered(input, "skl_EXISTING1"));
|
|
202
214
|
const result = await share({
|
|
203
215
|
paths: [file],
|
|
204
216
|
hqRoot,
|
|
205
217
|
entityContext: makeEntityContext(),
|
|
206
218
|
registerCompanySkill: registrar,
|
|
207
219
|
});
|
|
220
|
+
expect(registrar).toHaveBeenCalledOnce();
|
|
221
|
+
expect(registrar).toHaveBeenCalledWith({
|
|
222
|
+
companySlug: "acme",
|
|
223
|
+
path: "skills/existing/SKILL.md",
|
|
224
|
+
content: expect.stringContaining("skill_uid: skl_EXISTING1"),
|
|
225
|
+
landed: true,
|
|
226
|
+
});
|
|
227
|
+
expect(result.filesUploaded).toBe(1);
|
|
228
|
+
});
|
|
229
|
+
it("does not reconcile bytes edited while the upload is in flight", async () => {
|
|
230
|
+
const original = "---\nname: Existing\ndescription: Ready\nskill_uid: skl_EXISTING1\ntags: [sales]\n---\n";
|
|
231
|
+
const file = createSkill("existing", original);
|
|
232
|
+
const registrar = vi.fn(async (input) => registered(input, "skl_EXISTING1"));
|
|
233
|
+
vi.mocked(uploadFile).mockImplementationOnce(async () => {
|
|
234
|
+
fs.writeFileSync(file, original.replace("sales", "reporting"));
|
|
235
|
+
return { etag: '"upload-etag"' };
|
|
236
|
+
});
|
|
237
|
+
const result = await share({
|
|
238
|
+
paths: [file],
|
|
239
|
+
hqRoot,
|
|
240
|
+
entityContext: makeEntityContext(),
|
|
241
|
+
registerCompanySkill: registrar,
|
|
242
|
+
});
|
|
243
|
+
expect(result.filesUploaded).toBe(1);
|
|
208
244
|
expect(registrar).not.toHaveBeenCalled();
|
|
245
|
+
});
|
|
246
|
+
it("retries a failed post-upload metadata reconciliation without re-uploading", async () => {
|
|
247
|
+
const file = createSkill("existing", "---\nname: Existing\ndescription: Ready\nskill_uid: skl_EXISTING1\ntags: [sales]\n---\n");
|
|
248
|
+
const failedRegistrar = vi.fn(async () => {
|
|
249
|
+
throw new Error("registry temporarily unavailable");
|
|
250
|
+
});
|
|
251
|
+
const first = await share({
|
|
252
|
+
paths: [file],
|
|
253
|
+
hqRoot,
|
|
254
|
+
entityContext: makeEntityContext(),
|
|
255
|
+
registerCompanySkill: failedRegistrar,
|
|
256
|
+
});
|
|
257
|
+
expect(first.filesUploaded).toBe(1);
|
|
258
|
+
expect(readJournal("acme").files["skills/existing/SKILL.md"])
|
|
259
|
+
.toMatchObject({ skillMetadataPending: true });
|
|
260
|
+
vi.mocked(uploadFile).mockClear();
|
|
261
|
+
vi.mocked(headRemoteFile).mockResolvedValue({
|
|
262
|
+
lastModified: new Date(),
|
|
263
|
+
etag: '"upload-etag"',
|
|
264
|
+
size: fs.statSync(file).size,
|
|
265
|
+
});
|
|
266
|
+
const healingRegistrar = vi.fn(async (input) => registered(input, "skl_EXISTING1"));
|
|
267
|
+
const second = await share({
|
|
268
|
+
paths: [file],
|
|
269
|
+
hqRoot,
|
|
270
|
+
entityContext: makeEntityContext(),
|
|
271
|
+
registerCompanySkill: healingRegistrar,
|
|
272
|
+
skipUnchanged: true,
|
|
273
|
+
});
|
|
274
|
+
expect(second.filesUploaded).toBe(0);
|
|
275
|
+
expect(uploadFile).not.toHaveBeenCalled();
|
|
276
|
+
expect(healingRegistrar).toHaveBeenCalledWith(expect.objectContaining({ landed: true }));
|
|
277
|
+
expect(readJournal("acme").files["skills/existing/SKILL.md"]
|
|
278
|
+
?.skillMetadataPending).toBeUndefined();
|
|
279
|
+
});
|
|
280
|
+
it("durably records pending skill metadata before starting reconciliation", async () => {
|
|
281
|
+
const file = createSkill("existing", "---\nname: Existing\ndescription: Ready\nskill_uid: skl_EXISTING1\ntags: [sales]\n---\n");
|
|
282
|
+
let pendingAtReconcile;
|
|
283
|
+
const registrar = vi.fn(async (input) => {
|
|
284
|
+
pendingAtReconcile = readJournal("acme").files["skills/existing/SKILL.md"];
|
|
285
|
+
return registered(input, "skl_EXISTING1");
|
|
286
|
+
});
|
|
287
|
+
await share({
|
|
288
|
+
paths: [file],
|
|
289
|
+
hqRoot,
|
|
290
|
+
entityContext: makeEntityContext(),
|
|
291
|
+
registerCompanySkill: registrar,
|
|
292
|
+
});
|
|
293
|
+
expect(registrar).toHaveBeenCalledOnce();
|
|
294
|
+
expect(pendingAtReconcile)
|
|
295
|
+
.toMatchObject({ skillMetadataPending: true, remoteEtag: "upload-etag" });
|
|
296
|
+
});
|
|
297
|
+
it("reconciles skill metadata when restart finds identical remote bytes without a journal", async () => {
|
|
298
|
+
const content = "---\nname: Existing\ndescription: Ready\nskill_uid: skl_EXISTING1\ntags: [sales]\n---\n";
|
|
299
|
+
const file = createSkill("existing", content);
|
|
300
|
+
vi.mocked(headRemoteFile).mockResolvedValueOnce({
|
|
301
|
+
lastModified: new Date(),
|
|
302
|
+
etag: '"remote-etag"',
|
|
303
|
+
size: Buffer.byteLength(content),
|
|
304
|
+
});
|
|
305
|
+
vi.mocked(downloadFile).mockImplementationOnce(async (_ctx, _key, destination) => {
|
|
306
|
+
fs.writeFileSync(destination, content);
|
|
307
|
+
return undefined;
|
|
308
|
+
});
|
|
309
|
+
const registrar = vi.fn(async (input) => registered(input, "skl_EXISTING1"));
|
|
310
|
+
const result = await share({
|
|
311
|
+
paths: [file],
|
|
312
|
+
hqRoot,
|
|
313
|
+
entityContext: makeEntityContext(),
|
|
314
|
+
registerCompanySkill: registrar,
|
|
315
|
+
});
|
|
316
|
+
expect(result.filesUploaded).toBe(0);
|
|
317
|
+
expect(uploadFile).not.toHaveBeenCalled();
|
|
318
|
+
expect(registrar).toHaveBeenCalledWith(expect.objectContaining({
|
|
319
|
+
path: "skills/existing/SKILL.md",
|
|
320
|
+
content,
|
|
321
|
+
landed: true,
|
|
322
|
+
}));
|
|
323
|
+
expect(readJournal("acme").files["skills/existing/SKILL.md"])
|
|
324
|
+
.not.toHaveProperty("skillMetadataPending", true);
|
|
325
|
+
});
|
|
326
|
+
it("restarts instead of reconciling skill bytes edited after the equality proof", async () => {
|
|
327
|
+
const original = "---\nname: Existing\ndescription: Ready\nskill_uid: skl_EXISTING1\ntags: [sales]\n---\n";
|
|
328
|
+
const changed = original.replace("[sales]", "[reporting]");
|
|
329
|
+
const file = createSkill("existing", original);
|
|
330
|
+
vi.mocked(headRemoteFile).mockResolvedValue({
|
|
331
|
+
lastModified: new Date(),
|
|
332
|
+
etag: '"remote-etag"',
|
|
333
|
+
size: Buffer.byteLength(original),
|
|
334
|
+
});
|
|
335
|
+
vi.mocked(downloadFile).mockImplementation(async (_ctx, _key, destination) => {
|
|
336
|
+
fs.writeFileSync(destination, original);
|
|
337
|
+
return undefined;
|
|
338
|
+
});
|
|
339
|
+
const realReadFileSync = fs.readFileSync.bind(fs);
|
|
340
|
+
let skillReads = 0;
|
|
341
|
+
const readSpy = vi.spyOn(fs, "readFileSync").mockImplementation(((pathname, options) => {
|
|
342
|
+
if (pathname === file && options === undefined) {
|
|
343
|
+
skillReads++;
|
|
344
|
+
if (skillReads === 2)
|
|
345
|
+
fs.writeFileSync(file, changed);
|
|
346
|
+
}
|
|
347
|
+
return realReadFileSync(pathname, options);
|
|
348
|
+
}));
|
|
349
|
+
const registrar = vi.fn(async (input) => registered(input, "skl_EXISTING1"));
|
|
350
|
+
let result;
|
|
351
|
+
try {
|
|
352
|
+
result = await share({
|
|
353
|
+
paths: [file],
|
|
354
|
+
hqRoot,
|
|
355
|
+
entityContext: makeEntityContext(),
|
|
356
|
+
registerCompanySkill: registrar,
|
|
357
|
+
onConflict: "overwrite",
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
finally {
|
|
361
|
+
readSpy.mockRestore();
|
|
362
|
+
}
|
|
363
|
+
expect(result.filesUploaded).toBe(1);
|
|
364
|
+
expect(uploadFile).toHaveBeenCalledOnce();
|
|
365
|
+
expect(registrar).toHaveBeenCalledOnce();
|
|
366
|
+
expect(registrar).toHaveBeenCalledWith(expect.objectContaining({
|
|
367
|
+
content: changed,
|
|
368
|
+
landed: true,
|
|
369
|
+
}));
|
|
370
|
+
});
|
|
371
|
+
it("reconciles current remote metadata when the landed upload was replaced", async () => {
|
|
372
|
+
const file = createSkill("existing", "---\nname: Existing\ndescription: Ready\nskill_uid: skl_EXISTING1\ntags: [sales]\n---\n");
|
|
373
|
+
const failedRegistrar = vi.fn(async () => {
|
|
374
|
+
throw new Error("registry temporarily unavailable");
|
|
375
|
+
});
|
|
376
|
+
await share({
|
|
377
|
+
paths: [file],
|
|
378
|
+
hqRoot,
|
|
379
|
+
entityContext: makeEntityContext(),
|
|
380
|
+
registerCompanySkill: failedRegistrar,
|
|
381
|
+
});
|
|
382
|
+
expect(readJournal("acme").files["skills/existing/SKILL.md"])
|
|
383
|
+
.toMatchObject({ skillMetadataPending: true, remoteEtag: "upload-etag" });
|
|
384
|
+
vi.mocked(uploadFile).mockClear();
|
|
385
|
+
vi.mocked(headRemoteFile).mockResolvedValue({
|
|
386
|
+
lastModified: new Date(),
|
|
387
|
+
etag: '"peer-overwrite"',
|
|
388
|
+
size: fs.statSync(file).size,
|
|
389
|
+
});
|
|
390
|
+
const remote = fs.readFileSync(file, "utf8").replace("[sales]", "[remote]");
|
|
391
|
+
vi.mocked(downloadFile).mockImplementation(async (_ctx, _key, destination) => {
|
|
392
|
+
fs.writeFileSync(destination, remote);
|
|
393
|
+
return {};
|
|
394
|
+
});
|
|
395
|
+
const healingRegistrar = vi.fn(async (input) => registered(input, "skl_EXISTING1"));
|
|
396
|
+
const result = await share({
|
|
397
|
+
paths: [file],
|
|
398
|
+
hqRoot,
|
|
399
|
+
entityContext: makeEntityContext(),
|
|
400
|
+
registerCompanySkill: healingRegistrar,
|
|
401
|
+
skipUnchanged: true,
|
|
402
|
+
onConflict: "keep",
|
|
403
|
+
});
|
|
404
|
+
expect(healingRegistrar).toHaveBeenCalledWith(expect.objectContaining({ content: remote, landed: true }));
|
|
405
|
+
expect(readJournal("acme").files["skills/existing/SKILL.md"]
|
|
406
|
+
?.skillMetadataPending).toBeUndefined();
|
|
407
|
+
expect(result.conflictPaths).toEqual(["skills/existing/SKILL.md"]);
|
|
408
|
+
expect(fs.readFileSync(file, "utf8")).toBe(remote);
|
|
409
|
+
});
|
|
410
|
+
it("preserves entityContext-only uploads of already registered skills", async () => {
|
|
411
|
+
const file = createSkill("existing", "---\nname: Existing\nskill_uid: skl_EXISTING1\n---\n");
|
|
412
|
+
const result = await share({
|
|
413
|
+
paths: [file],
|
|
414
|
+
hqRoot,
|
|
415
|
+
entityContext: makeEntityContext(),
|
|
416
|
+
});
|
|
209
417
|
expect(result.filesUploaded).toBe(1);
|
|
418
|
+
expect(readJournal("acme").files["skills/existing/SKILL.md"])
|
|
419
|
+
.not.toHaveProperty("skillMetadataPending", true);
|
|
420
|
+
});
|
|
421
|
+
it("invalidates a replaced remote baseline even when metadata recovery fails", async () => {
|
|
422
|
+
const file = createSkill("existing", "---\nname: Existing\nskill_uid: skl_EXISTING1\n---\n");
|
|
423
|
+
await share({
|
|
424
|
+
paths: [file],
|
|
425
|
+
hqRoot,
|
|
426
|
+
entityContext: makeEntityContext(),
|
|
427
|
+
registerCompanySkill: vi.fn(async () => {
|
|
428
|
+
throw new Error("registry unavailable");
|
|
429
|
+
}),
|
|
430
|
+
});
|
|
431
|
+
vi.mocked(headRemoteFile).mockResolvedValue({
|
|
432
|
+
lastModified: new Date(),
|
|
433
|
+
etag: '"peer-overwrite"',
|
|
434
|
+
size: fs.statSync(file).size,
|
|
435
|
+
});
|
|
436
|
+
vi.mocked(downloadFile).mockRejectedValueOnce(new Error("download failed"));
|
|
437
|
+
await expect(share({
|
|
438
|
+
paths: [file],
|
|
439
|
+
hqRoot,
|
|
440
|
+
entityContext: makeEntityContext(),
|
|
441
|
+
registerCompanySkill: vi.fn(async (input) => registered(input, "skl_EXISTING1")),
|
|
442
|
+
skipUnchanged: true,
|
|
443
|
+
})).rejects.toThrow("download failed");
|
|
444
|
+
expect(readJournal("acme").files["skills/existing/SKILL.md"])
|
|
445
|
+
.toBeUndefined();
|
|
446
|
+
});
|
|
447
|
+
it("uploads a local change before reconciling metadata when it lands during remote verification", async () => {
|
|
448
|
+
const original = "---\nname: Existing\ndescription: Ready\nskill_uid: skl_EXISTING1\ntags: [sales]\n---\n";
|
|
449
|
+
const file = createSkill("existing", original);
|
|
450
|
+
const failedRegistrar = vi.fn(async () => {
|
|
451
|
+
throw new Error("registry temporarily unavailable");
|
|
452
|
+
});
|
|
453
|
+
await share({
|
|
454
|
+
paths: [file],
|
|
455
|
+
hqRoot,
|
|
456
|
+
entityContext: makeEntityContext(),
|
|
457
|
+
registerCompanySkill: failedRegistrar,
|
|
458
|
+
});
|
|
459
|
+
vi.mocked(uploadFile).mockClear();
|
|
460
|
+
vi.mocked(headRemoteFile).mockImplementationOnce(async () => {
|
|
461
|
+
fs.writeFileSync(file, original.replace("[sales]", "[reporting]"));
|
|
462
|
+
return {
|
|
463
|
+
lastModified: new Date(),
|
|
464
|
+
etag: '"upload-etag"',
|
|
465
|
+
size: Buffer.byteLength(original),
|
|
466
|
+
};
|
|
467
|
+
});
|
|
468
|
+
const healingRegistrar = vi.fn(async (input) => registered(input, "skl_EXISTING1"));
|
|
469
|
+
await share({
|
|
470
|
+
paths: [file],
|
|
471
|
+
hqRoot,
|
|
472
|
+
entityContext: makeEntityContext(),
|
|
473
|
+
registerCompanySkill: healingRegistrar,
|
|
474
|
+
skipUnchanged: true,
|
|
475
|
+
});
|
|
476
|
+
expect(uploadFile).toHaveBeenCalledTimes(1);
|
|
477
|
+
expect(healingRegistrar).toHaveBeenCalledTimes(1);
|
|
478
|
+
expect(vi.mocked(uploadFile).mock.invocationCallOrder[0]).toBeLessThan(healingRegistrar.mock.invocationCallOrder[0]);
|
|
479
|
+
expect(readJournal("acme").files["skills/existing/SKILL.md"]
|
|
480
|
+
?.skillMetadataPending).toBeUndefined();
|
|
210
481
|
});
|
|
211
482
|
it("recognizes quoted identities in CRLF frontmatter", () => {
|
|
212
483
|
expect(parseRegisteredCompanySkillUid("---\r\nname: Existing\r\nskill_uid: \"skl_WINDOWS1\"\r\n---\r\n")).toBe("skl_WINDOWS1");
|
|
@@ -1710,7 +1981,7 @@ describe("share", () => {
|
|
|
1710
1981
|
path: "v7b-priv.txt",
|
|
1711
1982
|
target: "../target.md",
|
|
1712
1983
|
}));
|
|
1713
|
-
expect(events.
|
|
1984
|
+
expect(events.filter((e) => e.type === "error")).toEqual([]);
|
|
1714
1985
|
// The conflict is still surfaced; the failed adoption restores local.
|
|
1715
1986
|
expect(result.conflictPaths).toEqual(["v7b-priv.txt"]);
|
|
1716
1987
|
expect(fs.readFileSync(testFile, "utf-8")).toBe("local-collision-content");
|
|
@@ -7697,12 +7968,23 @@ describe("nested companies directories in a company vault", () => {
|
|
|
7697
7968
|
const midPath = path.join(companyRoot, "audit", "companies", "notes.md");
|
|
7698
7969
|
fs.mkdirSync(path.dirname(midPath), { recursive: true });
|
|
7699
7970
|
fs.writeFileSync(midPath, "ordinary segment");
|
|
7971
|
+
const skillRegistrar = async (input) => ({
|
|
7972
|
+
ok: true,
|
|
7973
|
+
skillUid: "skl_SIGNALS1",
|
|
7974
|
+
path: input.path,
|
|
7975
|
+
vaultPath: input.path,
|
|
7976
|
+
content: input.content,
|
|
7977
|
+
action: "reused",
|
|
7978
|
+
accessPolicy: "preserved",
|
|
7979
|
+
skill: { name: "Signals", description: "", tags: [] },
|
|
7980
|
+
});
|
|
7700
7981
|
const events = [];
|
|
7701
7982
|
const result = await share({
|
|
7702
7983
|
paths: [companyRoot],
|
|
7703
7984
|
company: "acme",
|
|
7704
7985
|
vaultConfig: mockConfig,
|
|
7705
7986
|
hqRoot: tmpDir,
|
|
7987
|
+
registerCompanySkill: skillRegistrar,
|
|
7706
7988
|
onEvent: (e) => events.push(e),
|
|
7707
7989
|
});
|
|
7708
7990
|
expect(result.aborted).toBe(false);
|
|
@@ -7716,7 +7998,7 @@ describe("nested companies directories in a company vault", () => {
|
|
|
7716
7998
|
"audit/companies/notes.md",
|
|
7717
7999
|
]));
|
|
7718
8000
|
expect(events.filter((e) => e.type === "skip-invalid-scoped-key")).toEqual([]);
|
|
7719
|
-
expect(events.
|
|
8001
|
+
expect(events.filter((e) => e.type === "error")).toEqual([]);
|
|
7720
8002
|
});
|
|
7721
8003
|
it("never plans an upload for macOS Finder Icon\\r files — silent skip, valid files still ship", async () => {
|
|
7722
8004
|
// The incident: Finder writes an `Icon\r` file into every directory it
|
|
@@ -7993,4 +8275,45 @@ describe("defaultConsoleLogger — delete refusal wording per reason", () => {
|
|
|
7993
8275
|
expect(lines.join("\n")).toContain("ignore-filtered=2, excluded-top-level=1");
|
|
7994
8276
|
});
|
|
7995
8277
|
});
|
|
8278
|
+
describe("share reconcile apply checkpoint", () => {
|
|
8279
|
+
let hqRoot;
|
|
8280
|
+
let stateDir;
|
|
8281
|
+
beforeEach(() => {
|
|
8282
|
+
hqRoot = fs.mkdtempSync(path.join(os.tmpdir(), "hq-share-reconcile-"));
|
|
8283
|
+
stateDir = fs.mkdtempSync(path.join(os.tmpdir(), "hq-share-reconcile-state-"));
|
|
8284
|
+
process.env.HQ_STATE_DIR = stateDir;
|
|
8285
|
+
vi.mocked(uploadFile).mockReset();
|
|
8286
|
+
vi.mocked(uploadFile).mockRejectedValue(new Error("temporary upload failure"));
|
|
8287
|
+
vi.mocked(headRemoteFile).mockResolvedValue(null);
|
|
8288
|
+
vi.mocked(primeUploads).mockResolvedValue(undefined);
|
|
8289
|
+
vi.mocked(primeObjectTransport).mockResolvedValue(undefined);
|
|
8290
|
+
});
|
|
8291
|
+
afterEach(() => {
|
|
8292
|
+
fs.rmSync(hqRoot, { recursive: true, force: true });
|
|
8293
|
+
fs.rmSync(stateDir, { recursive: true, force: true });
|
|
8294
|
+
delete process.env.HQ_STATE_DIR;
|
|
8295
|
+
});
|
|
8296
|
+
it("withholds an apply checkpoint after a per-path upload failure", async () => {
|
|
8297
|
+
const localPath = path.join(hqRoot, "companies", "acme", "notes", "retry.md");
|
|
8298
|
+
fs.mkdirSync(path.dirname(localPath), { recursive: true });
|
|
8299
|
+
fs.writeFileSync(localPath, "retry me");
|
|
8300
|
+
const store = ReconcileCursorStore.open({
|
|
8301
|
+
rootDir: stateDir,
|
|
8302
|
+
journalSlug: "acme",
|
|
8303
|
+
layoutEpoch: 1,
|
|
8304
|
+
areaId: "root:notes",
|
|
8305
|
+
});
|
|
8306
|
+
const result = await share({
|
|
8307
|
+
paths: [localPath],
|
|
8308
|
+
hqRoot,
|
|
8309
|
+
entityContext: makeEntityContext(),
|
|
8310
|
+
reconcileApply: {
|
|
8311
|
+
store,
|
|
8312
|
+
cursor: { routerVersion: 1, epoch: "epoch-1", areaId: "root:notes", phase: "apply", journalAfterKey: "notes/retry.md" },
|
|
8313
|
+
},
|
|
8314
|
+
});
|
|
8315
|
+
expect(result.filesUploaded).toBe(0);
|
|
8316
|
+
expect(store.read().cursor).toBeUndefined();
|
|
8317
|
+
});
|
|
8318
|
+
});
|
|
7996
8319
|
//# sourceMappingURL=share.test.js.map
|