@indigoai-us/hq-cloud 6.15.34 → 6.15.35

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.
@@ -52,7 +52,7 @@ vi.mock("readline", () => ({
52
52
  })),
53
53
  }));
54
54
  import * as readline from "readline";
55
- import { share, isForbiddenCompanyVaultKey, UnreachablePushPathsError, currentUploadBytes, _testing as shareTesting } from "./share.js";
55
+ import { share, isForbiddenCompanyVaultKey, ServerOwnedPushPathsError, UnreachablePushPathsError, currentUploadBytes, _testing as shareTesting } from "./share.js";
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";
@@ -1222,6 +1222,110 @@ describe("share", () => {
1222
1222
  expect(fs.readFileSync(path.join(tmpDir, index.conflicts[0].conflictPath), "utf-8"))
1223
1223
  .toBe("local edit");
1224
1224
  });
1225
+ it.each([
1226
+ ["local-only", false],
1227
+ ["divergent-remote", true],
1228
+ ])("direct source push (%s): emits a machine-readable refusal and fails when it is the only requested path", async (_state, divergentRemote) => {
1229
+ const companyRoot = path.join(tmpDir, "companies", "acme");
1230
+ const testFile = path.join(companyRoot, "sources", "meetings", "m-1.md");
1231
+ fs.mkdirSync(path.dirname(testFile), { recursive: true });
1232
+ fs.writeFileSync(testFile, "local meeting source");
1233
+ if (divergentRemote) {
1234
+ seedJournalAt(path.join(stateDir, "sync-journal.acme.json"), {
1235
+ version: "1",
1236
+ lastSync: new Date().toISOString(),
1237
+ files: {
1238
+ "sources/meetings/m-1.md": {
1239
+ hash: "remote-body-hash",
1240
+ size: 21,
1241
+ syncedAt: new Date().toISOString(),
1242
+ direction: "down",
1243
+ remoteEtag: "remote-etag",
1244
+ localDiverges: true,
1245
+ },
1246
+ },
1247
+ });
1248
+ }
1249
+ const events = [];
1250
+ await expect(share({
1251
+ paths: [testFile],
1252
+ company: "acme",
1253
+ vaultConfig: mockConfig,
1254
+ hqRoot: tmpDir,
1255
+ skipUnchanged: divergentRemote,
1256
+ onEvent: (event) => events.push(event),
1257
+ })).rejects.toMatchObject({
1258
+ name: "ServerOwnedPushPathsError",
1259
+ paths: ["sources/meetings/m-1.md"],
1260
+ });
1261
+ expect(events).toContainEqual({
1262
+ type: "push-refused-server-owned",
1263
+ path: "sources/meetings/m-1.md",
1264
+ nextStep: "use-source-ingestion",
1265
+ });
1266
+ expect(events.some((event) => event.type === "reconciled")).toBe(false);
1267
+ expect(uploadFile).not.toHaveBeenCalled();
1268
+ expect(headRemoteFile).not.toHaveBeenCalled();
1269
+ });
1270
+ it.each([
1271
+ ["local-only", false],
1272
+ ["divergent-remote", true],
1273
+ ])("watch-driven source push (%s): explicit skip mode refuses without failing the pass", async (_state, divergentRemote) => {
1274
+ const companyRoot = path.join(tmpDir, "companies", "acme");
1275
+ const testFile = path.join(companyRoot, "sources", "meetings", "m-2.md");
1276
+ fs.mkdirSync(path.dirname(testFile), { recursive: true });
1277
+ fs.writeFileSync(testFile, "local meeting source");
1278
+ if (divergentRemote) {
1279
+ seedJournalAt(path.join(stateDir, "sync-journal.acme.json"), {
1280
+ version: "1",
1281
+ lastSync: new Date().toISOString(),
1282
+ files: {
1283
+ "sources/meetings/m-2.md": {
1284
+ hash: "remote-body-hash",
1285
+ size: 21,
1286
+ syncedAt: new Date().toISOString(),
1287
+ direction: "down",
1288
+ remoteEtag: "remote-etag",
1289
+ localDiverges: true,
1290
+ },
1291
+ },
1292
+ });
1293
+ }
1294
+ const events = [];
1295
+ const result = await share({
1296
+ paths: [testFile],
1297
+ company: "acme",
1298
+ vaultConfig: mockConfig,
1299
+ hqRoot: tmpDir,
1300
+ skipUnchanged: true,
1301
+ serverOwnedPathPolicy: "skip",
1302
+ onEvent: (event) => events.push(event),
1303
+ });
1304
+ expect(result).toMatchObject({ filesUploaded: 0, filesSkipped: 1 });
1305
+ expect(events).toContainEqual({
1306
+ type: "push-refused-server-owned",
1307
+ path: "sources/meetings/m-2.md",
1308
+ nextStep: "use-source-ingestion",
1309
+ });
1310
+ expect(events.some((event) => event.type === "reconciled")).toBe(false);
1311
+ expect(uploadFile).not.toHaveBeenCalled();
1312
+ expect(headRemoteFile).not.toHaveBeenCalled();
1313
+ });
1314
+ it("direct source push explains the server-owned refusal and supported next step", async () => {
1315
+ const companyRoot = path.join(tmpDir, "companies", "acme");
1316
+ const testFile = path.join(companyRoot, "sources", "meetings", "m-3.md");
1317
+ fs.mkdirSync(path.dirname(testFile), { recursive: true });
1318
+ fs.writeFileSync(testFile, "local meeting source");
1319
+ const warn = vi.spyOn(console, "warn").mockImplementation(() => undefined);
1320
+ await expect(share({
1321
+ paths: [testFile],
1322
+ company: "acme",
1323
+ vaultConfig: mockConfig,
1324
+ hqRoot: tmpDir,
1325
+ })).rejects.toThrow(ServerOwnedPushPathsError);
1326
+ expect(warn).toHaveBeenCalledWith(expect.stringContaining("is server-owned and was not uploaded"));
1327
+ expect(warn).toHaveBeenCalledWith(expect.stringContaining("supported integration or ingestion flow"));
1328
+ });
1225
1329
  it("cloud-authoritative: never pushes a stale local ontology/.last-run over the server value (one-sided-change clobber guard)", async () => {
1226
1330
  // Regression for the gardener watermark clobber. A second HQ root sharing this
1227
1331
  // machine's sync journal can leave the local mirror holding a STALE value A while
@@ -1255,11 +1359,14 @@ describe("share", () => {
1255
1359
  },
1256
1360
  },
1257
1361
  }));
1362
+ const events = [];
1258
1363
  const result = await share({
1259
1364
  paths: [testFile],
1260
1365
  company: "acme",
1261
1366
  vaultConfig: mockConfig,
1262
1367
  hqRoot: tmpDir,
1368
+ serverOwnedPathPolicy: "skip",
1369
+ onEvent: (event) => events.push(event),
1263
1370
  });
1264
1371
  // Server-owned file: push is skipped BEFORE HEAD, the stale local value is NEVER
1265
1372
  // uploaded (the pull leg refreshes local from cloud instead), and it is not a conflict.
@@ -1268,6 +1375,12 @@ describe("share", () => {
1268
1375
  expect(result.filesUploaded).toBe(0);
1269
1376
  expect(result.filesSkipped).toBeGreaterThanOrEqual(1);
1270
1377
  expect(result.conflictPaths).toEqual([]);
1378
+ expect(events).toContainEqual({
1379
+ type: "push-refused-server-owned",
1380
+ path: "ontology/.last-run",
1381
+ nextStep: "change-upstream-and-pull",
1382
+ });
1383
+ expect(events.some((event) => event.type === "reconciled")).toBe(false);
1271
1384
  });
1272
1385
  it("first-time-upload-with-cloud-collision: emits conflict + writes mirror under --on-conflict keep (Bug #7)", async () => {
1273
1386
  // Bug #7 (data-loss class) from the 5.33.0 deep test: when a file has