@indigoai-us/hq-cloud 6.14.3 → 6.14.5
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-company.d.ts +1 -0
- package/dist/bin/sync-runner-company.d.ts.map +1 -1
- package/dist/bin/sync-runner-company.js +44 -19
- package/dist/bin/sync-runner-company.js.map +1 -1
- package/dist/bin/sync-runner.d.ts.map +1 -1
- package/dist/bin/sync-runner.js +25 -36
- package/dist/bin/sync-runner.js.map +1 -1
- package/dist/bin/sync-runner.test.js +128 -4
- package/dist/bin/sync-runner.test.js.map +1 -1
- package/dist/cli/reindex.d.ts.map +1 -1
- package/dist/cli/reindex.js +209 -29
- package/dist/cli/reindex.js.map +1 -1
- package/dist/cli/reindex.test.js +116 -37
- package/dist/cli/reindex.test.js.map +1 -1
- package/dist/cli/rescue-clone-diagnostics.test.d.ts +2 -0
- package/dist/cli/rescue-clone-diagnostics.test.d.ts.map +1 -0
- package/dist/cli/rescue-clone-diagnostics.test.js +101 -0
- package/dist/cli/rescue-clone-diagnostics.test.js.map +1 -0
- package/dist/cli/rescue-core.d.ts.map +1 -1
- package/dist/cli/rescue-core.js +28 -6
- package/dist/cli/rescue-core.js.map +1 -1
- package/dist/cli/share.js +11 -22
- package/dist/cli/share.js.map +1 -1
- package/dist/cli/share.test.js +40 -0
- package/dist/cli/share.test.js.map +1 -1
- package/dist/cli/sync.d.ts.map +1 -1
- package/dist/cli/sync.js +16 -14
- package/dist/cli/sync.js.map +1 -1
- package/dist/cli/sync.test.js +73 -0
- package/dist/cli/sync.test.js.map +1 -1
- package/dist/watcher.d.ts +1 -1
- package/dist/watcher.d.ts.map +1 -1
- package/dist/watcher.js +5 -6
- package/dist/watcher.js.map +1 -1
- package/dist/watcher.test.js +54 -0
- package/dist/watcher.test.js.map +1 -1
- package/package.json +1 -1
- package/src/bin/sync-runner-company.ts +50 -22
- package/src/bin/sync-runner.test.ts +147 -4
- package/src/bin/sync-runner.ts +49 -54
- package/src/cli/reindex.test.ts +130 -37
- package/src/cli/reindex.ts +198 -28
- package/src/cli/rescue-clone-diagnostics.test.ts +120 -0
- package/src/cli/rescue-core.ts +32 -8
- package/src/cli/share.test.ts +47 -0
- package/src/cli/share.ts +16 -26
- package/src/cli/sync.test.ts +90 -0
- package/src/cli/sync.ts +16 -14
- package/src/watcher.test.ts +59 -0
- package/src/watcher.ts +6 -8
|
@@ -1604,6 +1604,34 @@ describe("all-complete aggregate", () => {
|
|
|
1604
1604
|
expect(deps.stderr.raw()).toContain("runner.telemetry.timeout");
|
|
1605
1605
|
});
|
|
1606
1606
|
|
|
1607
|
+
it("silences no-op completion and company-leg telemetry but retains session start", async () => {
|
|
1608
|
+
const posts: TelemetryEventsBatch[] = [];
|
|
1609
|
+
const client: VaultClientSurface = {
|
|
1610
|
+
...makeVaultStub({
|
|
1611
|
+
memberships: [{ companyUid: "cmp_a" }],
|
|
1612
|
+
entityGet: (uid: string) =>
|
|
1613
|
+
Promise.resolve({ uid, slug: "acme" } as unknown as EntityInfo),
|
|
1614
|
+
}),
|
|
1615
|
+
postTelemetryEvents: vi.fn(async (batch) => {
|
|
1616
|
+
posts.push(batch);
|
|
1617
|
+
return { ok: true, written: batch.events.length, skipped: [] };
|
|
1618
|
+
}),
|
|
1619
|
+
};
|
|
1620
|
+
const deps = makeDeps({ createVaultClient: () => client });
|
|
1621
|
+
|
|
1622
|
+
const code = await runRunner(["--companies"], deps);
|
|
1623
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
1624
|
+
|
|
1625
|
+
expect(code).toBe(0);
|
|
1626
|
+
const eventNames = posts
|
|
1627
|
+
.flatMap((post) => post.events)
|
|
1628
|
+
.map((event) => event.eventName);
|
|
1629
|
+
expect(eventNames).toContain("sync_runner_session_started");
|
|
1630
|
+
expect(eventNames).not.toContain("sync_runner_plan_built");
|
|
1631
|
+
expect(eventNames).not.toContain("company_sync_leg_completed");
|
|
1632
|
+
expect(eventNames).not.toContain("sync_runner_completed");
|
|
1633
|
+
});
|
|
1634
|
+
|
|
1607
1635
|
it("emits action telemetry for completion and telemetry transport failure does not block completion", async () => {
|
|
1608
1636
|
const posts: TelemetryEventsBatch[] = [];
|
|
1609
1637
|
const client: VaultClientSurface = {
|
|
@@ -1626,11 +1654,25 @@ describe("all-complete aggregate", () => {
|
|
|
1626
1654
|
defaultSyncResult({
|
|
1627
1655
|
filesDownloaded: 2,
|
|
1628
1656
|
bytesDownloaded: 300,
|
|
1657
|
+
filesTombstoned: 3,
|
|
1658
|
+
scopeOrphansRemoved: 4,
|
|
1659
|
+
scopeOrphansBlocked: 5,
|
|
1660
|
+
}),
|
|
1661
|
+
),
|
|
1662
|
+
share: vi
|
|
1663
|
+
.fn<(opts: ShareOptions) => Promise<ShareResult>>()
|
|
1664
|
+
.mockResolvedValue(
|
|
1665
|
+
defaultShareResult({
|
|
1666
|
+
filesUploaded: 6,
|
|
1667
|
+
bytesUploaded: 700,
|
|
1668
|
+
filesDeleted: 7,
|
|
1669
|
+
filesTombstoned: 8,
|
|
1670
|
+
filesRefusedStale: 9,
|
|
1629
1671
|
}),
|
|
1630
1672
|
),
|
|
1631
1673
|
});
|
|
1632
1674
|
|
|
1633
|
-
const code = await runRunner(["--companies"], deps);
|
|
1675
|
+
const code = await runRunner(["--companies", "--direction", "both"], deps);
|
|
1634
1676
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
1635
1677
|
|
|
1636
1678
|
expect(code).toBe(0);
|
|
@@ -1645,19 +1687,120 @@ describe("all-complete aggregate", () => {
|
|
|
1645
1687
|
source: "sync-runner",
|
|
1646
1688
|
schemaVersion: 1,
|
|
1647
1689
|
properties: {
|
|
1648
|
-
|
|
1690
|
+
status: "completed",
|
|
1649
1691
|
attemptedCount: 1,
|
|
1650
1692
|
companyCount: 1,
|
|
1651
1693
|
errorCount: 0,
|
|
1652
1694
|
filesDownloaded: 2,
|
|
1653
1695
|
bytesDownloaded: 300,
|
|
1654
|
-
filesUploaded:
|
|
1655
|
-
bytesUploaded:
|
|
1696
|
+
filesUploaded: 6,
|
|
1697
|
+
bytesUploaded: 700,
|
|
1698
|
+
},
|
|
1699
|
+
});
|
|
1700
|
+
const companyLeg = posts
|
|
1701
|
+
.flatMap((post) => post.events)
|
|
1702
|
+
.find((event) => event.eventName === "company_sync_leg_completed");
|
|
1703
|
+
expect(companyLeg).toMatchObject({
|
|
1704
|
+
properties: {
|
|
1705
|
+
status: "complete",
|
|
1706
|
+
deletedCount: 7,
|
|
1707
|
+
tombstonedCount: 11,
|
|
1708
|
+
scopeOrphansRemovedCount: 4,
|
|
1709
|
+
refusedStaleCount: 9,
|
|
1710
|
+
scopeOrphansBlockedCount: 5,
|
|
1656
1711
|
},
|
|
1657
1712
|
});
|
|
1658
1713
|
expect(deps.stderr.raw()).toContain("runner.telemetry_events.nonfatal");
|
|
1659
1714
|
});
|
|
1660
1715
|
|
|
1716
|
+
it("retains terminal and company-leg telemetry for a partial sync", async () => {
|
|
1717
|
+
const posts: TelemetryEventsBatch[] = [];
|
|
1718
|
+
const client: VaultClientSurface = {
|
|
1719
|
+
...makeVaultStub({
|
|
1720
|
+
memberships: [{ companyUid: "cmp_a" }],
|
|
1721
|
+
entityGet: (uid: string) =>
|
|
1722
|
+
Promise.resolve({ uid, slug: "acme" } as unknown as EntityInfo),
|
|
1723
|
+
}),
|
|
1724
|
+
postTelemetryEvents: vi.fn(async (batch) => {
|
|
1725
|
+
posts.push(batch);
|
|
1726
|
+
return { ok: true, written: batch.events.length, skipped: [] };
|
|
1727
|
+
}),
|
|
1728
|
+
};
|
|
1729
|
+
const deps = makeDeps({
|
|
1730
|
+
createVaultClient: () => client,
|
|
1731
|
+
sync: vi
|
|
1732
|
+
.fn<(opts: SyncOptions) => Promise<SyncResult>>()
|
|
1733
|
+
.mockResolvedValue(defaultSyncResult({ aborted: true })),
|
|
1734
|
+
});
|
|
1735
|
+
|
|
1736
|
+
const code = await runRunner(["--companies"], deps);
|
|
1737
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
1738
|
+
|
|
1739
|
+
expect(code).toBe(0);
|
|
1740
|
+
const events = posts.flatMap((post) => post.events);
|
|
1741
|
+
expect(events.find((event) => event.eventName === "company_sync_leg_completed"))
|
|
1742
|
+
.toMatchObject({ properties: { status: "aborted" } });
|
|
1743
|
+
expect(events.find((event) => event.eventName === "sync_runner_completed"))
|
|
1744
|
+
.toMatchObject({ properties: { status: "partial" } });
|
|
1745
|
+
});
|
|
1746
|
+
|
|
1747
|
+
it("retains terminal and company-leg telemetry when a sync leg throws", async () => {
|
|
1748
|
+
const posts: TelemetryEventsBatch[] = [];
|
|
1749
|
+
const client: VaultClientSurface = {
|
|
1750
|
+
...makeVaultStub({
|
|
1751
|
+
memberships: [{ companyUid: "cmp_a" }],
|
|
1752
|
+
entityGet: (uid: string) =>
|
|
1753
|
+
Promise.resolve({ uid, slug: "acme" } as unknown as EntityInfo),
|
|
1754
|
+
}),
|
|
1755
|
+
postTelemetryEvents: vi.fn(async (batch) => {
|
|
1756
|
+
posts.push(batch);
|
|
1757
|
+
return { ok: true, written: batch.events.length, skipped: [] };
|
|
1758
|
+
}),
|
|
1759
|
+
};
|
|
1760
|
+
const deps = makeDeps({
|
|
1761
|
+
createVaultClient: () => client,
|
|
1762
|
+
sync: vi
|
|
1763
|
+
.fn<(opts: SyncOptions) => Promise<SyncResult>>()
|
|
1764
|
+
.mockRejectedValue(new Error("sync failed")),
|
|
1765
|
+
});
|
|
1766
|
+
|
|
1767
|
+
const code = await runRunner(["--companies"], deps);
|
|
1768
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
1769
|
+
|
|
1770
|
+
expect(code).toBe(PARTIAL_SYNC_EXIT);
|
|
1771
|
+
const events = posts.flatMap((post) => post.events);
|
|
1772
|
+
expect(events.find((event) => event.eventName === "company_sync_leg_completed"))
|
|
1773
|
+
.toMatchObject({ properties: { status: "errored" } });
|
|
1774
|
+
expect(events.find((event) => event.eventName === "sync_runner_completed"))
|
|
1775
|
+
.toMatchObject({ properties: { status: "partial" } });
|
|
1776
|
+
});
|
|
1777
|
+
|
|
1778
|
+
it("retains terminal failure telemetry when post-client discovery fails", async () => {
|
|
1779
|
+
const posts: TelemetryEventsBatch[] = [];
|
|
1780
|
+
const client: VaultClientSurface = {
|
|
1781
|
+
...makeVaultStub({
|
|
1782
|
+
memberships: [{ companyUid: "cmp_a" }],
|
|
1783
|
+
}),
|
|
1784
|
+
listMyMemberships: () => Promise.reject(new Error("network down")),
|
|
1785
|
+
postTelemetryEvents: vi.fn(async (batch) => {
|
|
1786
|
+
posts.push(batch);
|
|
1787
|
+
return { ok: true, written: batch.events.length, skipped: [] };
|
|
1788
|
+
}),
|
|
1789
|
+
};
|
|
1790
|
+
const deps = makeDeps({ createVaultClient: () => client });
|
|
1791
|
+
|
|
1792
|
+
const code = await runRunner(["--companies"], deps);
|
|
1793
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
1794
|
+
|
|
1795
|
+
expect(code).toBe(1);
|
|
1796
|
+
expect(posts.flatMap((post) => post.events)).toContainEqual(
|
|
1797
|
+
expect.objectContaining({
|
|
1798
|
+
eventName: "sync_runner_completed",
|
|
1799
|
+
properties: { status: "failed" },
|
|
1800
|
+
}),
|
|
1801
|
+
);
|
|
1802
|
+
});
|
|
1803
|
+
|
|
1661
1804
|
it("sums filesDownloaded and bytesDownloaded across all companies", async () => {
|
|
1662
1805
|
const slugs: Record<string, string> = { cmp_a: "acme", cmp_b: "beta" };
|
|
1663
1806
|
const deps = makeDeps({
|
package/src/bin/sync-runner.ts
CHANGED
|
@@ -1166,6 +1166,37 @@ export async function runRunner(
|
|
|
1166
1166
|
},
|
|
1167
1167
|
);
|
|
1168
1168
|
|
|
1169
|
+
const emitRunnerTerminalTelemetry = (properties: {
|
|
1170
|
+
status: "completed" | "partial" | "failed";
|
|
1171
|
+
attemptedCount?: number;
|
|
1172
|
+
companyCount?: number;
|
|
1173
|
+
errorCount?: number;
|
|
1174
|
+
filesDownloaded?: number;
|
|
1175
|
+
bytesDownloaded?: number;
|
|
1176
|
+
filesUploaded?: number;
|
|
1177
|
+
bytesUploaded?: number;
|
|
1178
|
+
}): void => {
|
|
1179
|
+
void emitCloudTelemetry(
|
|
1180
|
+
client,
|
|
1181
|
+
{
|
|
1182
|
+
eventName: "sync_runner_completed",
|
|
1183
|
+
source: "sync-runner",
|
|
1184
|
+
sessionId: telemetrySessionId,
|
|
1185
|
+
properties,
|
|
1186
|
+
},
|
|
1187
|
+
{
|
|
1188
|
+
claims,
|
|
1189
|
+
log: (message) =>
|
|
1190
|
+
reportDiagnostic({
|
|
1191
|
+
component: "telemetry-events",
|
|
1192
|
+
event: "runner.telemetry_events.nonfatal",
|
|
1193
|
+
message,
|
|
1194
|
+
context: { source: "sync-runner" },
|
|
1195
|
+
}),
|
|
1196
|
+
},
|
|
1197
|
+
);
|
|
1198
|
+
};
|
|
1199
|
+
|
|
1169
1200
|
// ---- transport selection (presigned-URL for cmp_, direct-S3/STS for prs_) -
|
|
1170
1201
|
// Company vaults ALWAYS use the presigned-URL transport now (HQ-59): the
|
|
1171
1202
|
// STS-direct-S3 path for cmp_ is retired — no env override routes a company
|
|
@@ -1222,6 +1253,7 @@ export async function runRunner(
|
|
|
1222
1253
|
message: err instanceof Error ? err.message : String(err),
|
|
1223
1254
|
path: "(discovery)",
|
|
1224
1255
|
});
|
|
1256
|
+
emitRunnerTerminalTelemetry({ status: "failed" });
|
|
1225
1257
|
// A transient network failure (offline, DNS blip, vault API briefly
|
|
1226
1258
|
// unreachable) is NOT a crash — it self-heals on the next poll. Return the
|
|
1227
1259
|
// retryable exit code so the watch loop stays alive instead of exiting and
|
|
@@ -1246,31 +1278,6 @@ export async function runRunner(
|
|
|
1246
1278
|
}
|
|
1247
1279
|
const plan = targetPlan.plan;
|
|
1248
1280
|
|
|
1249
|
-
void emitCloudTelemetry(
|
|
1250
|
-
client,
|
|
1251
|
-
{
|
|
1252
|
-
eventName: "sync_runner_plan_built",
|
|
1253
|
-
source: "sync-runner",
|
|
1254
|
-
sessionId: telemetrySessionId,
|
|
1255
|
-
properties: {
|
|
1256
|
-
direction: parsed.direction,
|
|
1257
|
-
targetCount: plan.length,
|
|
1258
|
-
companyCount: plan.filter((target) => target.personalMode !== true).length,
|
|
1259
|
-
personalCount: plan.filter((target) => target.personalMode === true).length,
|
|
1260
|
-
},
|
|
1261
|
-
},
|
|
1262
|
-
{
|
|
1263
|
-
claims,
|
|
1264
|
-
log: (message) =>
|
|
1265
|
-
reportDiagnostic({
|
|
1266
|
-
component: "telemetry-events",
|
|
1267
|
-
event: "runner.telemetry_events.nonfatal",
|
|
1268
|
-
message,
|
|
1269
|
-
context: { source: "sync-runner" },
|
|
1270
|
-
}),
|
|
1271
|
-
},
|
|
1272
|
-
);
|
|
1273
|
-
|
|
1274
1281
|
emitFanoutPlan(emit, plan);
|
|
1275
1282
|
|
|
1276
1283
|
// One-time seed of the reserved personal-vault journal from the legacy
|
|
@@ -1301,7 +1308,7 @@ export async function runRunner(
|
|
|
1301
1308
|
telemetryClaims: claims,
|
|
1302
1309
|
telemetrySessionId,
|
|
1303
1310
|
});
|
|
1304
|
-
const { errors, allConflicts } = fanout;
|
|
1311
|
+
const { errors, allConflicts, meaningfulOutcomeCount } = fanout;
|
|
1305
1312
|
const rollup = rollupAllComplete(plan, fanout.stateByCompany);
|
|
1306
1313
|
|
|
1307
1314
|
if (rollup.needsReindex) {
|
|
@@ -1414,34 +1421,22 @@ export async function runRunner(
|
|
|
1414
1421
|
partial: rollup.partial,
|
|
1415
1422
|
companies: rollup.companies,
|
|
1416
1423
|
});
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
},
|
|
1434
|
-
{
|
|
1435
|
-
claims,
|
|
1436
|
-
log: (message) =>
|
|
1437
|
-
reportDiagnostic({
|
|
1438
|
-
component: "telemetry-events",
|
|
1439
|
-
event: "runner.telemetry_events.nonfatal",
|
|
1440
|
-
message,
|
|
1441
|
-
context: { source: "sync-runner" },
|
|
1442
|
-
}),
|
|
1443
|
-
},
|
|
1444
|
-
);
|
|
1424
|
+
if (
|
|
1425
|
+
rollup.partial ||
|
|
1426
|
+
errors.length > 0 ||
|
|
1427
|
+
meaningfulOutcomeCount > 0
|
|
1428
|
+
) {
|
|
1429
|
+
emitRunnerTerminalTelemetry({
|
|
1430
|
+
status: rollup.partial || errors.length > 0 ? "partial" : "completed",
|
|
1431
|
+
attemptedCount: plan.length,
|
|
1432
|
+
companyCount: rollup.companies.length,
|
|
1433
|
+
errorCount: errors.length,
|
|
1434
|
+
filesDownloaded: rollup.totalDownloaded,
|
|
1435
|
+
bytesDownloaded: rollup.totalDownloadedBytes,
|
|
1436
|
+
filesUploaded: rollup.totalUploaded,
|
|
1437
|
+
bytesUploaded: rollup.totalUploadedBytes,
|
|
1438
|
+
});
|
|
1439
|
+
}
|
|
1445
1440
|
|
|
1446
1441
|
// Post-sync qmd reindex — runs AFTER `all-complete` is emitted so the
|
|
1447
1442
|
// menubar/CLI already shows the sync as done; this is a best-effort tail
|
package/src/cli/reindex.test.ts
CHANGED
|
@@ -558,70 +558,163 @@ describe("reindex", () => {
|
|
|
558
558
|
).toBe(true);
|
|
559
559
|
});
|
|
560
560
|
|
|
561
|
-
// --- session-log capture into workspace/.session-logs
|
|
561
|
+
// --- multi-harness session-log capture into workspace/.session-logs/<harness>/
|
|
562
562
|
describe("session-log capture", () => {
|
|
563
|
-
let
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
563
|
+
let claudeDir: string; // CLAUDE_CONFIG_DIR
|
|
564
|
+
let codexHome: string; // CODEX_HOME
|
|
565
|
+
let grokHome: string; // GROK_HOME
|
|
566
|
+
|
|
567
|
+
// Per-harness cwd encodings the harnesses actually use.
|
|
568
|
+
const claudeSlug = (p: string): string => p.replace(/[^a-zA-Z0-9]/g, "-");
|
|
569
|
+
const grokKey = (p: string): string => encodeURIComponent(p);
|
|
570
|
+
|
|
571
|
+
const dest = (): string => path.join(root, "workspace", ".session-logs");
|
|
567
572
|
|
|
568
573
|
beforeEach(() => {
|
|
569
|
-
|
|
570
|
-
|
|
574
|
+
claudeDir = fs.mkdtempSync(path.join(os.tmpdir(), "ms-claude-"));
|
|
575
|
+
codexHome = fs.mkdtempSync(path.join(os.tmpdir(), "ms-codex-"));
|
|
576
|
+
grokHome = fs.mkdtempSync(path.join(os.tmpdir(), "ms-grok-"));
|
|
577
|
+
process.env.CLAUDE_CONFIG_DIR = claudeDir;
|
|
578
|
+
process.env.CODEX_HOME = codexHome;
|
|
579
|
+
process.env.GROK_HOME = grokHome;
|
|
571
580
|
});
|
|
572
581
|
|
|
573
582
|
afterEach(() => {
|
|
574
|
-
|
|
583
|
+
for (const d of [claudeDir, codexHome, grokHome]) {
|
|
584
|
+
fs.rmSync(d, { recursive: true, force: true });
|
|
585
|
+
}
|
|
575
586
|
delete process.env.CLAUDE_CONFIG_DIR;
|
|
587
|
+
delete process.env.CODEX_HOME;
|
|
588
|
+
delete process.env.GROK_HOME;
|
|
576
589
|
});
|
|
577
590
|
|
|
578
|
-
|
|
579
|
-
|
|
591
|
+
// Seed a Claude project folder for cwd `cwdPath`, return its dest dir name.
|
|
592
|
+
function seedClaude(cwdPath: string, body = "claude\n"): string {
|
|
593
|
+
const name = claudeSlug(cwdPath);
|
|
594
|
+
const projDir = path.join(claudeDir, "projects", name);
|
|
580
595
|
fs.mkdirSync(path.join(projDir, "tool-results"), { recursive: true });
|
|
581
|
-
fs.writeFileSync(path.join(projDir, "session.jsonl"),
|
|
582
|
-
fs.writeFileSync(path.join(projDir, "tool-results", "r.txt"), "
|
|
583
|
-
return
|
|
596
|
+
fs.writeFileSync(path.join(projDir, "session.jsonl"), body);
|
|
597
|
+
fs.writeFileSync(path.join(projDir, "tool-results", "r.txt"), "res\n");
|
|
598
|
+
return name;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
// Seed a Grok session folder for cwd `cwdPath`, return its dest dir name.
|
|
602
|
+
function seedGrok(cwdPath: string, body = "grok\n"): string {
|
|
603
|
+
const name = grokKey(cwdPath);
|
|
604
|
+
const sess = path.join(grokHome, "sessions", name, "uuid-1");
|
|
605
|
+
fs.mkdirSync(sess, { recursive: true });
|
|
606
|
+
fs.writeFileSync(path.join(sess, "chat_history.jsonl"), body);
|
|
607
|
+
return name;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
// Seed a Codex rollout at YYYY/MM/DD/<file> whose session_meta records `cwd`.
|
|
611
|
+
function seedCodex(cwd: string, date: [string, string, string], file: string): string {
|
|
612
|
+
const [y, m, d] = date;
|
|
613
|
+
const dir = path.join(codexHome, "sessions", y, m, d);
|
|
614
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
615
|
+
const meta = JSON.stringify({ type: "session_meta", payload: { cwd } });
|
|
616
|
+
const event = JSON.stringify({ type: "event_msg", payload: { text: "hi" } });
|
|
617
|
+
const abs = path.join(dir, file);
|
|
618
|
+
fs.writeFileSync(abs, `${meta}\n${event}\n`);
|
|
619
|
+
return abs;
|
|
584
620
|
}
|
|
585
621
|
|
|
586
|
-
|
|
587
|
-
|
|
622
|
+
// ── Claude ──────────────────────────────────────────────────────────────
|
|
623
|
+
it("claude: captures the HQ-root project AND descendant projects under claude/", () => {
|
|
624
|
+
const rootName = seedClaude(root);
|
|
625
|
+
const subName = seedClaude(path.join(root, "workspace", "worktrees", "x"));
|
|
626
|
+
// An unrelated project (different root) must NOT be captured.
|
|
627
|
+
seedClaude("/some/other/place");
|
|
628
|
+
|
|
629
|
+
expect(reindex({ repoRoot: root }).status).toBe(0);
|
|
630
|
+
|
|
631
|
+
const c = path.join(dest(), "claude");
|
|
632
|
+
expect(fs.readFileSync(path.join(c, rootName, "session.jsonl"), "utf8")).toBe("claude\n");
|
|
633
|
+
expect(fs.existsSync(path.join(c, subName, "session.jsonl"))).toBe(true);
|
|
634
|
+
expect(fs.existsSync(path.join(c, claudeSlug("/some/other/place")))).toBe(false);
|
|
635
|
+
// Namespaced: claude logs are NOT dumped at the .session-logs root anymore.
|
|
636
|
+
expect(fs.existsSync(path.join(dest(), "session.jsonl"))).toBe(false);
|
|
637
|
+
});
|
|
638
|
+
|
|
639
|
+
it("claude: overwrites an existing dest file but never prunes dest-only entries", () => {
|
|
640
|
+
const rootName = seedClaude(root, "fresh\n");
|
|
641
|
+
const c = path.join(dest(), "claude");
|
|
642
|
+
fs.mkdirSync(path.join(c, rootName), { recursive: true });
|
|
643
|
+
fs.writeFileSync(path.join(c, rootName, "session.jsonl"), "STALE\n");
|
|
644
|
+
fs.writeFileSync(path.join(dest(), "orphan.txt"), "keep\n");
|
|
645
|
+
|
|
646
|
+
expect(reindex({ repoRoot: root }).status).toBe(0);
|
|
647
|
+
|
|
648
|
+
expect(fs.readFileSync(path.join(c, rootName, "session.jsonl"), "utf8")).toBe("fresh\n");
|
|
649
|
+
expect(fs.readFileSync(path.join(dest(), "orphan.txt"), "utf8")).toBe("keep\n");
|
|
650
|
+
});
|
|
651
|
+
|
|
652
|
+
// ── Grok ────────────────────────────────────────────────────────────────
|
|
653
|
+
it("grok: captures the HQ-root and descendant cwd folders under grok/, excludes others", () => {
|
|
654
|
+
const rootName = seedGrok(root);
|
|
655
|
+
const subName = seedGrok(path.join(root, "repos", "private", "x"));
|
|
656
|
+
seedGrok("/tmp/benchmark"); // unrelated cwd → excluded
|
|
657
|
+
|
|
658
|
+
expect(reindex({ repoRoot: root }).status).toBe(0);
|
|
659
|
+
|
|
660
|
+
const g = path.join(dest(), "grok");
|
|
661
|
+
expect(fs.readFileSync(path.join(g, rootName, "uuid-1", "chat_history.jsonl"), "utf8")).toBe("grok\n");
|
|
662
|
+
expect(fs.existsSync(path.join(g, subName, "uuid-1", "chat_history.jsonl"))).toBe(true);
|
|
663
|
+
expect(fs.existsSync(path.join(g, grokKey("/tmp/benchmark")))).toBe(false);
|
|
664
|
+
});
|
|
665
|
+
|
|
666
|
+
// ── Codex ───────────────────────────────────────────────────────────────
|
|
667
|
+
it("codex: captures rollouts whose recorded cwd is the HQ tree, mirroring the date path", () => {
|
|
668
|
+
seedCodex(root, ["2026", "07", "13"], "rollout-root.jsonl");
|
|
669
|
+
seedCodex(path.join(root, "workspace", "worktrees", "y"), ["2026", "07", "13"], "rollout-sub.jsonl");
|
|
670
|
+
seedCodex("/elsewhere/repo", ["2026", "07", "13"], "rollout-other.jsonl");
|
|
588
671
|
|
|
589
672
|
expect(reindex({ repoRoot: root }).status).toBe(0);
|
|
590
673
|
|
|
591
|
-
const
|
|
592
|
-
expect(fs.
|
|
593
|
-
expect(fs.
|
|
674
|
+
const cx = path.join(dest(), "codex", "2026", "07", "13");
|
|
675
|
+
expect(fs.existsSync(path.join(cx, "rollout-root.jsonl"))).toBe(true);
|
|
676
|
+
expect(fs.existsSync(path.join(cx, "rollout-sub.jsonl"))).toBe(true);
|
|
677
|
+
// A rollout run from outside the HQ tree is never captured (tenant scope).
|
|
678
|
+
expect(fs.existsSync(path.join(cx, "rollout-other.jsonl"))).toBe(false);
|
|
594
679
|
});
|
|
595
680
|
|
|
596
|
-
it("
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
681
|
+
it("codex: skips a rollout already captured and unchanged (mtime guard)", () => {
|
|
682
|
+
const src = seedCodex(root, ["2026", "07", "13"], "rollout-a.jsonl");
|
|
683
|
+
expect(reindex({ repoRoot: root }).status).toBe(0);
|
|
684
|
+
|
|
685
|
+
// Tamper the dest copy and push its mtime into the future; an unchanged
|
|
686
|
+
// src must be skipped, leaving the sentinel in place.
|
|
687
|
+
const destFile = path.join(dest(), "codex", "2026", "07", "13", "rollout-a.jsonl");
|
|
688
|
+
fs.writeFileSync(destFile, "SENTINEL\n");
|
|
689
|
+
const future = Date.now() / 1000 + 3600;
|
|
690
|
+
fs.utimesSync(destFile, future, future);
|
|
601
691
|
|
|
602
692
|
expect(reindex({ repoRoot: root }).status).toBe(0);
|
|
693
|
+
expect(fs.readFileSync(destFile, "utf8")).toBe("SENTINEL\n");
|
|
603
694
|
|
|
604
|
-
|
|
695
|
+
// But when the src is newer, it IS re-copied (overwriting the sentinel).
|
|
696
|
+
fs.writeFileSync(src, `${JSON.stringify({ type: "session_meta", payload: { cwd: root } })}\nNEW\n`);
|
|
697
|
+
const soon = Date.now() / 1000 + 7200;
|
|
698
|
+
fs.utimesSync(src, soon, soon);
|
|
699
|
+
expect(reindex({ repoRoot: root }).status).toBe(0);
|
|
700
|
+
expect(fs.readFileSync(destFile, "utf8")).toContain("NEW");
|
|
605
701
|
});
|
|
606
702
|
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
fs.writeFileSync(path.join(dest, "orphan.txt"), "orphan\n");
|
|
703
|
+
// ── Cross-harness ─────────────────────────────────────────────────────────
|
|
704
|
+
it("all three harnesses land side-by-side under their own subfolders", () => {
|
|
705
|
+
seedClaude(root);
|
|
706
|
+
seedGrok(root);
|
|
707
|
+
seedCodex(root, ["2026", "07", "14"], "rollout-z.jsonl");
|
|
613
708
|
|
|
614
709
|
expect(reindex({ repoRoot: root }).status).toBe(0);
|
|
615
710
|
|
|
616
|
-
|
|
617
|
-
expect(fs.existsSync(path.join(dest, "
|
|
618
|
-
|
|
619
|
-
expect(fs.readFileSync(path.join(dest, "old-run", "keep.jsonl"), "utf8")).toBe("keep\n");
|
|
620
|
-
expect(fs.readFileSync(path.join(dest, "orphan.txt"), "utf8")).toBe("orphan\n");
|
|
711
|
+
expect(fs.existsSync(path.join(dest(), "claude", claudeSlug(root), "session.jsonl"))).toBe(true);
|
|
712
|
+
expect(fs.existsSync(path.join(dest(), "grok", grokKey(root), "uuid-1", "chat_history.jsonl"))).toBe(true);
|
|
713
|
+
expect(fs.existsSync(path.join(dest(), "codex", "2026", "07", "14", "rollout-z.jsonl"))).toBe(true);
|
|
621
714
|
});
|
|
622
715
|
|
|
623
|
-
it("no-ops (still exit 0) when
|
|
624
|
-
//
|
|
716
|
+
it("no-ops (still exit 0) when no harness has logs for this root", () => {
|
|
717
|
+
// Home dirs exist but are empty → nothing to capture, no dest created.
|
|
625
718
|
expect(reindex({ repoRoot: root }).status).toBe(0);
|
|
626
719
|
expect(fs.existsSync(path.join(root, "workspace", ".session-logs"))).toBe(false);
|
|
627
720
|
});
|