claude-code-session-manager 0.80.0 → 0.82.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.
Files changed (59) hide show
  1. package/dist/assets/{AgentLibrary-zS3jw_1e.js → AgentLibrary-pwlkAFb3.js} +1 -1
  2. package/dist/assets/{DataModel-Cy_vxTpi.js → DataModel-BZK9PXFD.js} +1 -1
  3. package/dist/assets/{History-C6JRuqfT.js → History-BVRjxJjS.js} +1 -1
  4. package/dist/assets/{Hooks-BafPy9mB.js → Hooks-CNuwHeGx.js} +1 -1
  5. package/dist/assets/{HostBilko-BZwhQOFt.js → HostBilko-cjwNodhV.js} +1 -1
  6. package/dist/assets/{Library-C8JDDliz.js → Library-YPNm9W92.js} +1 -1
  7. package/dist/assets/{ListDetail-CqiOdwLc.js → ListDetail-CY4GM1Om.js} +1 -1
  8. package/dist/assets/{MarkdownEditor-CyLyP67L.js → MarkdownEditor-BF4y2Jiz.js} +1 -1
  9. package/dist/assets/{McpServers-BzMv-_84.js → McpServers-CmBdWtX_.js} +1 -1
  10. package/dist/assets/{Memory-DSBYQdJR.js → Memory-CvkIXNl1.js} +1 -1
  11. package/dist/assets/{Panel-CLUhkNNA.js → Panel-D93o-sxe.js} +1 -1
  12. package/dist/assets/{Permissions-BfC2-HN4.js → Permissions-DQipg16I.js} +1 -1
  13. package/dist/assets/{Plugins-BKi40jT5.js → Plugins-B6NwzPfK.js} +2 -2
  14. package/dist/assets/{ProvenanceBadge-BzFw4KhD.js → ProvenanceBadge-DACVJhrB.js} +1 -1
  15. package/dist/assets/{SaveBar-avk2p9jv.js → SaveBar-Cg4lbChb.js} +1 -1
  16. package/dist/assets/{Scheduler-Bf_6MdJo.js → Scheduler-Dr5ZcBLe.js} +1 -1
  17. package/dist/assets/{ScopeSwitcher-C-RwYUVZ.js → ScopeSwitcher-C-locvy0.js} +1 -1
  18. package/dist/assets/{Settings-Djd8OoBA.js → Settings-BVrAle90.js} +1 -1
  19. package/dist/assets/{SkillReferenceGraph-DuogY6s7.js → SkillReferenceGraph-DDzuYgSK.js} +1 -1
  20. package/dist/assets/{Skills-D_qAqxZ_.js → Skills-DFvhiOAQ.js} +1 -1
  21. package/dist/assets/{SystemPrompt-DbHFLQV3.js → SystemPrompt-8PiTyUyL.js} +1 -1
  22. package/dist/assets/{TagLibrary-C2y91BT0.js → TagLibrary-DruUYaAc.js} +1 -1
  23. package/dist/assets/{TiptapBody-D9iz4xQx.js → TiptapBody-Dr4a--42.js} +1 -1
  24. package/dist/assets/{Toggle-BGnFL2E5.js → Toggle-B_EH2TFb.js} +1 -1
  25. package/dist/assets/{index-_2ARyFDj.js → index-DApB4DHS.js} +4 -4
  26. package/dist/assets/{settingsSchema-JK15eJU8.js → settingsSchema-BKa-xk8g.js} +1 -1
  27. package/dist/index.html +1 -1
  28. package/package.json +1 -1
  29. package/plugins/session-manager-dev/skills/develop/standards.md +1 -0
  30. package/scripts/project-pages-logic/dist/logic.cjs +12 -12
  31. package/scripts/render-project-pages/dist/renderer.cjs +22 -22
  32. package/src/main/__tests__/machineProfile.test.cjs +134 -0
  33. package/src/main/__tests__/rcaReport.test.cjs +24 -0
  34. package/src/main/__tests__/runVerify-blocked-by-foreign-wip.test.cjs +58 -0
  35. package/src/main/__tests__/runVerify-policy-denial.test.cjs +89 -0
  36. package/src/main/__tests__/scheduler-already-satisfied-on-main.test.cjs +105 -0
  37. package/src/main/__tests__/scheduler-blocked-by-foreign-wip.test.cjs +107 -0
  38. package/src/main/__tests__/scheduler-finalize-dispatch-guards.test.cjs +229 -0
  39. package/src/main/__tests__/scheduler-looks-done.test.cjs +141 -1
  40. package/src/main/__tests__/scheduler-periodic-reverify-guard.test.cjs +85 -0
  41. package/src/main/__tests__/scheduler-rate-limit-pause.test.cjs +81 -0
  42. package/src/main/__tests__/scheduler-reap-dead-running-jobs.test.cjs +205 -2
  43. package/src/main/__tests__/telemetrySettings.test.cjs +178 -0
  44. package/src/main/lib/__tests__/branchSweep.test.cjs +164 -0
  45. package/src/main/lib/__tests__/fixtures/204-mercury-steam-horse.log.txt +13 -0
  46. package/src/main/lib/__tests__/landedSinceRun.test.cjs +61 -1
  47. package/src/main/lib/__tests__/rateLimitWindow.test.cjs +88 -0
  48. package/src/main/lib/__tests__/reaperHelpers.test.cjs +120 -1
  49. package/src/main/lib/branchSweep.cjs +127 -0
  50. package/src/main/lib/gitWorktree.cjs +20 -0
  51. package/src/main/lib/landedSinceRun.cjs +41 -1
  52. package/src/main/lib/machineProfile.cjs +144 -0
  53. package/src/main/lib/rateLimitWindow.cjs +62 -0
  54. package/src/main/lib/rcaReport.cjs +18 -3
  55. package/src/main/lib/reaperHelpers.cjs +169 -3
  56. package/src/main/lib/scheduleJobTransitions.cjs +16 -3
  57. package/src/main/lib/telemetrySettings.cjs +171 -0
  58. package/src/main/runVerify.cjs +71 -3
  59. package/src/main/scheduler.cjs +768 -34
@@ -0,0 +1,178 @@
1
+ /**
2
+ * telemetrySettings.test.cjs — unit tests for the telemetry consent/config store.
3
+ *
4
+ * Run: timeout 120 npx vitest run src/main/__tests__/telemetrySettings.test.cjs
5
+ */
6
+ 'use strict';
7
+
8
+ import { test, expect, beforeEach, afterEach } from 'vitest';
9
+ const fs = require('node:fs');
10
+ const fsp = require('node:fs/promises');
11
+ const os = require('node:os');
12
+ const path = require('node:path');
13
+
14
+ const tmpDirs = [];
15
+ let originalHome;
16
+
17
+ beforeEach(() => {
18
+ originalHome = process.env.HOME;
19
+ });
20
+
21
+ afterEach(async () => {
22
+ if (originalHome !== undefined) process.env.HOME = originalHome;
23
+ delete process.env.SM_TELEMETRY;
24
+ delete process.env.SM_TELEMETRY_ENDPOINT;
25
+ while (tmpDirs.length) {
26
+ const d = tmpDirs.pop();
27
+ await fsp.rm(d, { recursive: true, force: true });
28
+ }
29
+ });
30
+
31
+ async function freshModule(home) {
32
+ process.env.HOME = home;
33
+ const modPath = require.resolve('../lib/telemetrySettings.cjs');
34
+ delete require.cache[modPath];
35
+ delete require.cache[require.resolve('../config.cjs')];
36
+ return require('../lib/telemetrySettings.cjs');
37
+ }
38
+
39
+ async function mkHome() {
40
+ const dir = await fsp.mkdtemp(path.join(os.tmpdir(), 'sm-telemetry-home-'));
41
+ tmpDirs.push(dir);
42
+ return dir;
43
+ }
44
+
45
+ test('DEFAULTS carries no identity-shaped key', async () => {
46
+ const home = await mkHome();
47
+ const telemetrySettings = await freshModule(home);
48
+ const keys = Object.keys(telemetrySettings.DEFAULTS);
49
+ const identityLike = keys.filter((k) => /email|user|name|host|account/i.test(k));
50
+ expect(identityLike).toEqual([]);
51
+ });
52
+
53
+ test('isValid rejects a config carrying an unknown extra identity-shaped key', async () => {
54
+ const home = await mkHome();
55
+ const telemetrySettings = await freshModule(home);
56
+ const base = { ...telemetrySettings.DEFAULTS, installId: 'abc' };
57
+ expect(telemetrySettings.isValid(base)).toBe(true);
58
+ const withExtra = { ...base, identifyEmail: 'someone@example.com' };
59
+ expect(telemetrySettings.isValid(withExtra)).toBe(false);
60
+ });
61
+
62
+ test('load() mints installId once via crypto.randomUUID and persists it', async () => {
63
+ const home = await mkHome();
64
+ const telemetrySettings = await freshModule(home);
65
+
66
+ const first = await telemetrySettings.load();
67
+ expect(typeof first.installId).toBe('string');
68
+ expect(first.installId.length).toBeGreaterThan(0);
69
+
70
+ const second = await telemetrySettings.load();
71
+ expect(second.installId).toBe(first.installId);
72
+
73
+ const raw = JSON.parse(fs.readFileSync(telemetrySettings.storePath(), 'utf8'));
74
+ expect(raw.installId).toBe(first.installId);
75
+
76
+ const freshRead = await freshModule(home);
77
+ const third = await freshRead.load();
78
+ expect(third.installId).toBe(first.installId);
79
+ });
80
+
81
+ test('isEnabled: hard kill switch SM_TELEMETRY=0 overrides persisted enabled:true', async () => {
82
+ const home = await mkHome();
83
+ const telemetrySettings = await freshModule(home);
84
+ const cfg = { ...telemetrySettings.DEFAULTS, installId: 'x', enabled: true };
85
+ process.env.SM_TELEMETRY = '0';
86
+ expect(telemetrySettings.isEnabled(cfg)).toBe(false);
87
+ });
88
+
89
+ test('isEnabled: returns false when persisted enabled is false', async () => {
90
+ const home = await mkHome();
91
+ const telemetrySettings = await freshModule(home);
92
+ const cfg = { ...telemetrySettings.DEFAULTS, installId: 'x', enabled: false };
93
+ expect(telemetrySettings.isEnabled(cfg)).toBe(false);
94
+ });
95
+
96
+ test('isEnabled: returns true when persisted enabled is true and no kill switch', async () => {
97
+ const home = await mkHome();
98
+ const telemetrySettings = await freshModule(home);
99
+ const cfg = { ...telemetrySettings.DEFAULTS, installId: 'x', enabled: true };
100
+ expect(telemetrySettings.isEnabled(cfg)).toBe(true);
101
+ });
102
+
103
+ test('resolveEndpoint prefers SM_TELEMETRY_ENDPOINT over persisted endpoint', async () => {
104
+ const home = await mkHome();
105
+ const telemetrySettings = await freshModule(home);
106
+ const cfg = { ...telemetrySettings.DEFAULTS, installId: 'x', endpoint: 'https://persisted.example' };
107
+ process.env.SM_TELEMETRY_ENDPOINT = 'https://env-override.example';
108
+ expect(telemetrySettings.resolveEndpoint(cfg)).toBe('https://env-override.example');
109
+ });
110
+
111
+ test('resolveEndpoint falls back to persisted endpoint when no env override', async () => {
112
+ const home = await mkHome();
113
+ const telemetrySettings = await freshModule(home);
114
+ const cfg = { ...telemetrySettings.DEFAULTS, installId: 'x', endpoint: 'https://persisted.example' };
115
+ expect(telemetrySettings.resolveEndpoint(cfg)).toBe('https://persisted.example');
116
+ });
117
+
118
+ test('isDailyFlushDue: true when lastDailyFlushAt is null', async () => {
119
+ const home = await mkHome();
120
+ const telemetrySettings = await freshModule(home);
121
+ const cfg = { ...telemetrySettings.DEFAULTS, installId: 'x', lastDailyFlushAt: null };
122
+ expect(telemetrySettings.isDailyFlushDue(cfg, Date.now())).toBe(true);
123
+ });
124
+
125
+ test('isDailyFlushDue: false when lastDailyFlushAt is 23h before now', async () => {
126
+ const home = await mkHome();
127
+ const telemetrySettings = await freshModule(home);
128
+ const now = Date.now();
129
+ const cfg = { ...telemetrySettings.DEFAULTS, installId: 'x', lastDailyFlushAt: new Date(now - 23 * 60 * 60 * 1000).toISOString() };
130
+ expect(telemetrySettings.isDailyFlushDue(cfg, now)).toBe(false);
131
+ });
132
+
133
+ test('isDailyFlushDue: true when lastDailyFlushAt is 25h before now', async () => {
134
+ const home = await mkHome();
135
+ const telemetrySettings = await freshModule(home);
136
+ const now = Date.now();
137
+ const cfg = { ...telemetrySettings.DEFAULTS, installId: 'x', lastDailyFlushAt: new Date(now - 25 * 60 * 60 * 1000).toISOString() };
138
+ expect(telemetrySettings.isDailyFlushDue(cfg, now)).toBe(true);
139
+ });
140
+
141
+ test('isMachineReportDue: true when app version changed', async () => {
142
+ const home = await mkHome();
143
+ const telemetrySettings = await freshModule(home);
144
+ const now = Date.now();
145
+ const cfg = {
146
+ ...telemetrySettings.DEFAULTS,
147
+ installId: 'x',
148
+ lastMachineReportVersion: '0.80.0',
149
+ lastMachineReportAt: new Date(now).toISOString(),
150
+ };
151
+ expect(telemetrySettings.isMachineReportDue(cfg, { now, appVersion: '0.81.0' })).toBe(true);
152
+ });
153
+
154
+ test('isMachineReportDue: true when lastMachineReportAt is null (same version)', async () => {
155
+ const home = await mkHome();
156
+ const telemetrySettings = await freshModule(home);
157
+ const now = Date.now();
158
+ const cfg = { ...telemetrySettings.DEFAULTS, installId: 'x', lastMachineReportVersion: '0.81.0', lastMachineReportAt: null };
159
+ expect(telemetrySettings.isMachineReportDue(cfg, { now, appVersion: '0.81.0' })).toBe(true);
160
+ });
161
+
162
+ test('isMachineReportDue: true when lastMachineReportAt is more than 30 days before now (same version)', async () => {
163
+ const home = await mkHome();
164
+ const telemetrySettings = await freshModule(home);
165
+ const now = Date.now();
166
+ const thirtyOneDaysAgo = new Date(now - 31 * 24 * 60 * 60 * 1000).toISOString();
167
+ const cfg = { ...telemetrySettings.DEFAULTS, installId: 'x', lastMachineReportVersion: '0.81.0', lastMachineReportAt: thirtyOneDaysAgo };
168
+ expect(telemetrySettings.isMachineReportDue(cfg, { now, appVersion: '0.81.0' })).toBe(true);
169
+ });
170
+
171
+ test('isMachineReportDue: false when same version and reported less than 30 days ago', async () => {
172
+ const home = await mkHome();
173
+ const telemetrySettings = await freshModule(home);
174
+ const now = Date.now();
175
+ const oneDayAgo = new Date(now - 24 * 60 * 60 * 1000).toISOString();
176
+ const cfg = { ...telemetrySettings.DEFAULTS, installId: 'x', lastMachineReportVersion: '0.81.0', lastMachineReportAt: oneDayAgo };
177
+ expect(telemetrySettings.isMachineReportDue(cfg, { now, appVersion: '0.81.0' })).toBe(false);
178
+ });
@@ -0,0 +1,164 @@
1
+ /**
2
+ * branchSweep.test.cjs — PRD 1135: recovering `sm-job/<slug>` branches left
3
+ * stranded (unmerged, owning job row terminal or gone) never gets swept away
4
+ * by hand months later.
5
+ *
6
+ * Exercises the real `git` plumbing against throwaway repos under
7
+ * os.tmpdir() (fast — no network, no Electron).
8
+ *
9
+ * Run: timeout 120 npx vitest run src/main/lib/__tests__/branchSweep.test.cjs
10
+ */
11
+ 'use strict';
12
+
13
+ import { test, expect, beforeEach, afterEach } from 'vitest';
14
+ const fs = require('node:fs');
15
+ const os = require('node:os');
16
+ const path = require('node:path');
17
+ const { execFileSync } = require('node:child_process');
18
+ const { sweepStrandedJobBranches } = require('../branchSweep.cjs');
19
+
20
+ let tmpRoot;
21
+ let repoCwd;
22
+ let worktreeDir;
23
+
24
+ function git(args, cwd) {
25
+ return execFileSync('git', args, { cwd, encoding: 'utf8' });
26
+ }
27
+
28
+ function initRepo(dir) {
29
+ fs.mkdirSync(dir, { recursive: true });
30
+ git(['init', '-q'], dir);
31
+ git(['config', 'user.email', 'test@example.com'], dir);
32
+ git(['config', 'user.name', 'Test'], dir);
33
+ fs.writeFileSync(path.join(dir, 'README.md'), 'hello\n', 'utf8');
34
+ git(['add', '-A'], dir);
35
+ git(['commit', '-q', '-m', 'initial'], dir);
36
+ }
37
+
38
+ beforeEach(() => {
39
+ tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'sm-branchsweep-'));
40
+ repoCwd = path.join(tmpRoot, 'repo');
41
+ initRepo(repoCwd);
42
+ delete process.env.SM_BRANCH_SWEEP_DISABLE;
43
+ });
44
+
45
+ afterEach(() => {
46
+ if (worktreeDir && fs.existsSync(worktreeDir)) {
47
+ try { git(['worktree', 'remove', '--force', worktreeDir], repoCwd); } catch { /* best effort */ }
48
+ }
49
+ worktreeDir = undefined;
50
+ delete process.env.SM_BRANCH_SWEEP_DISABLE;
51
+ fs.rmSync(tmpRoot, { recursive: true, force: true });
52
+ });
53
+
54
+ /** Create an `sm-job/<slug>` branch with one commit beyond HEAD, without checking it out. */
55
+ function makeStrandedBranch(cwd, slug, fileContents = 'stranded work\n') {
56
+ const branch = `sm-job/${slug}`;
57
+ git(['branch', branch], cwd);
58
+ const tmpCheckout = fs.mkdtempSync(path.join(os.tmpdir(), 'sm-branchsweep-checkout-'));
59
+ git(['worktree', 'add', '-q', tmpCheckout, branch], cwd);
60
+ fs.writeFileSync(path.join(tmpCheckout, `${slug}.txt`), fileContents, 'utf8');
61
+ git(['add', '-A'], tmpCheckout);
62
+ git(['commit', '-q', '-m', `job ${slug} work`], tmpCheckout);
63
+ git(['worktree', 'remove', '--force', tmpCheckout], cwd);
64
+ return branch;
65
+ }
66
+
67
+ test('stranded branch with a terminal job row is integrated', async () => {
68
+ const branch = makeStrandedBranch(repoCwd, 'alpha');
69
+ const jobs = [{ slug: 'alpha', status: 'completed' }];
70
+ const { results } = await sweepStrandedJobBranches({ cwd: repoCwd, jobs, attemptedBranches: new Set() });
71
+ const r = results.find((x) => x.branch === branch);
72
+ expect(r.action).toBe('integrated');
73
+ expect(fs.existsSync(path.join(repoCwd, 'alpha.txt'))).toBe(true);
74
+ });
75
+
76
+ test('stranded branch with NO job row at all is also integrated', async () => {
77
+ const branch = makeStrandedBranch(repoCwd, 'ghost');
78
+ const { results } = await sweepStrandedJobBranches({ cwd: repoCwd, jobs: [], attemptedBranches: new Set() });
79
+ const r = results.find((x) => x.branch === branch);
80
+ expect(r.action).toBe('integrated');
81
+ });
82
+
83
+ test('branch already merged into HEAD is a silent no-op', async () => {
84
+ const branch = makeStrandedBranch(repoCwd, 'bravo');
85
+ git(['merge', '--no-ff', '--no-edit', branch], repoCwd);
86
+ const jobs = [{ slug: 'bravo', status: 'completed' }];
87
+ const { results } = await sweepStrandedJobBranches({ cwd: repoCwd, jobs, attemptedBranches: new Set() });
88
+ const r = results.find((x) => x.branch === branch);
89
+ expect(r.action).toBe('no-op-merged');
90
+ });
91
+
92
+ test('branch checked out in a live worktree is skipped entirely', async () => {
93
+ const branch = makeStrandedBranch(repoCwd, 'charlie');
94
+ worktreeDir = fs.mkdtempSync(path.join(os.tmpdir(), 'sm-branchsweep-live-'));
95
+ git(['worktree', 'add', '-q', worktreeDir, branch], repoCwd);
96
+ const jobs = [{ slug: 'charlie', status: 'running' }];
97
+ const { results } = await sweepStrandedJobBranches({ cwd: repoCwd, jobs, attemptedBranches: new Set() });
98
+ const r = results.find((x) => x.branch === branch);
99
+ expect(r.action).toBe('skip-checked-out');
100
+ });
101
+
102
+ test('branch owned by a NON-terminal job row is left alone, never integrated', async () => {
103
+ const branch = makeStrandedBranch(repoCwd, 'delta');
104
+ const jobs = [{ slug: 'delta', status: 'needs_review' }];
105
+ const { results } = await sweepStrandedJobBranches({ cwd: repoCwd, jobs, attemptedBranches: new Set() });
106
+ const r = results.find((x) => x.branch === branch);
107
+ expect(r.action).toBe('skip-active-row');
108
+ expect(fs.existsSync(path.join(repoCwd, 'delta.txt'))).toBe(false);
109
+ });
110
+
111
+ test('conflicting branch is flagged, never forced, and the branch is never deleted', async () => {
112
+ // Same path committed differently on both HEAD and the branch → real conflict.
113
+ fs.writeFileSync(path.join(repoCwd, 'shared.txt'), 'main tree version\n', 'utf8');
114
+ git(['add', '-A'], repoCwd);
115
+ git(['commit', '-q', '-m', 'main tree edits shared.txt'], repoCwd);
116
+
117
+ const branch = 'sm-job/echo';
118
+ git(['branch', branch, 'HEAD~1'], repoCwd);
119
+ const tmpCheckout = fs.mkdtempSync(path.join(os.tmpdir(), 'sm-branchsweep-conflict-'));
120
+ git(['worktree', 'add', '-q', tmpCheckout, branch], repoCwd);
121
+ fs.writeFileSync(path.join(tmpCheckout, 'shared.txt'), 'branch version — conflicts\n', 'utf8');
122
+ git(['add', '-A'], tmpCheckout);
123
+ git(['commit', '-q', '-m', 'job echo edits shared.txt'], tmpCheckout);
124
+ git(['worktree', 'remove', '--force', tmpCheckout], repoCwd);
125
+
126
+ const jobs = [{ slug: 'echo', status: 'failed' }];
127
+ const { results } = await sweepStrandedJobBranches({ cwd: repoCwd, jobs, attemptedBranches: new Set() });
128
+ const r = results.find((x) => x.branch === branch);
129
+ expect(r.action).toBe('conflict');
130
+ expect(r.integration.ok).toBe(false);
131
+ // Branch itself must survive a conflict, untouched.
132
+ const stillExists = git(['rev-parse', '--verify', branch], repoCwd).trim();
133
+ expect(stillExists.length).toBe(40);
134
+ });
135
+
136
+ test('a branch is retried at most once per process lifetime via the shared attempted set', async () => {
137
+ fs.writeFileSync(path.join(repoCwd, 'shared2.txt'), 'main\n', 'utf8');
138
+ git(['add', '-A'], repoCwd);
139
+ git(['commit', '-q', '-m', 'main edits shared2'], repoCwd);
140
+ const branch = 'sm-job/foxtrot';
141
+ git(['branch', branch, 'HEAD~1'], repoCwd);
142
+ const tmpCheckout = fs.mkdtempSync(path.join(os.tmpdir(), 'sm-branchsweep-retry-'));
143
+ git(['worktree', 'add', '-q', tmpCheckout, branch], repoCwd);
144
+ fs.writeFileSync(path.join(tmpCheckout, 'shared2.txt'), 'branch — conflicts\n', 'utf8');
145
+ git(['add', '-A'], tmpCheckout);
146
+ git(['commit', '-q', '-m', 'job foxtrot edits shared2'], tmpCheckout);
147
+ git(['worktree', 'remove', '--force', tmpCheckout], repoCwd);
148
+
149
+ const jobs = [{ slug: 'foxtrot', status: 'failed' }];
150
+ const attemptedBranches = new Set();
151
+ const first = await sweepStrandedJobBranches({ cwd: repoCwd, jobs, attemptedBranches });
152
+ expect(first.results.find((x) => x.branch === branch).action).toBe('conflict');
153
+ const second = await sweepStrandedJobBranches({ cwd: repoCwd, jobs, attemptedBranches });
154
+ expect(second.results.find((x) => x.branch === branch).action).toBe('skip-already-attempted');
155
+ });
156
+
157
+ test('SM_BRANCH_SWEEP_DISABLE=1 restores today\'s behaviour exactly — no scan, no integration', async () => {
158
+ makeStrandedBranch(repoCwd, 'golf');
159
+ process.env.SM_BRANCH_SWEEP_DISABLE = '1';
160
+ const jobs = [{ slug: 'golf', status: 'completed' }];
161
+ const { results } = await sweepStrandedJobBranches({ cwd: repoCwd, jobs, attemptedBranches: new Set() });
162
+ expect(results).toEqual([]);
163
+ expect(fs.existsSync(path.join(repoCwd, 'golf.txt'))).toBe(false);
164
+ });
@@ -0,0 +1,13 @@
1
+ [scheduler] starting 204-mercury-steam-horse at 2026-09-05T16:32:31.039Z
2
+ [scheduler] cwd=/home/bilko/Projects/starry-night-ships (running isolated in worktree /tmp/session-manager-job-worktrees/443997d5bbc5f8ab/204-mercury-steam-horse)
3
+
4
+ [scheduler] agentType=dev-lead persona=/home/bilko/.claude/agents/dev-lead.md model=fable
5
+ [scheduler] spawned pid=3537290 sessionId=5cb9d69b-e2e7-49f9-806c-2307f1c94b2b (process group)
6
+
7
+ {"type":"rate_limit_event","rate_limit_info":{"status":"allowed_warning","resetsAt":1789059600,"rateLimitType":"seven_day","utilization":0.59,"isUsingOverage":false,"unifiedWindows":{"five_hour":{"utilization":0,"resetsAt":1788643800},"seven_day":{"utilization":0.59,"resetsAt":1789059600}}},"uuid":"f7ef520a-9c3f-470e-89de-4ba9af7d5968","session_id":"5cb9d69b-e2e7-49f9-806c-2307f1c94b2b"}
8
+ {"type":"system","subtype":"init","cwd":"/tmp/session-manager-job-worktrees/443997d5bbc5f8ab/204-mercury-steam-horse","session_id":"5cb9d69b-e2e7-49f9-806c-2307f1c94b2b","tools":["Task","Bash","CronCreate","CronDelete","CronList","DesignSync","Edit","EnterWorktree","ExitWorktree","ListAgents","ListMcpResourcesTool","Monitor","NotebookEdit","PushNotification","Read","ReadMcpResourceDirTool","ReadMcpResourceTool","RemoteTrigger","ReportFindings","ScheduleWakeup","SendMessage","Skill","TaskOutput","TaskStop","ToolSearch","WebFetch","WebSearch","Workflow","Write","mcp__bilko-host__get_host_contract","mcp__bilko-host__list_projects","mcp__bilko-host__publish_static_project","mcp__bilko-host__register_static_project","mcp__bilko-host__status","mcp__bilko-host__unregister_project","mcp__bilko-host__usage_report","mcp__claude_ai_Gmail__apply_sensitive_message_label","mcp__claude_ai_Gmail__apply_sensitive_thread_label","mcp__claude_ai_Gmail__create_draft","mcp__claude_ai_Gmail__create_label","mcp__claude_ai_Gmail__delete_label","mcp__claude_ai_Gmail__forward","mcp__claude_ai_Gmail__get_draft","mcp__claude_ai_Gmail__get_message","mcp__claude_ai_Gmail__get_thread","mcp__claude_ai_Gmail__label_message","mcp__claude_ai_Gmail__label_thread","mcp__claude_ai_Gmail__list_drafts","mcp__claude_ai_Gmail__list_labels","mcp__claude_ai_Gmail__mark_message_spam","mcp__claude_ai_Gmail__mark_thread_spam","mcp__claude_ai_Gmail__reply","mcp__claude_ai_Gmail__search_threads","mcp__claude_ai_Gmail__send_message","mcp__claude_ai_Gmail__trash_message","mcp__claude_ai_Gmail__trash_thread","mcp__claude_ai_Gmail__unlabel_message","mcp__claude_ai_Gmail__unlabel_thread","mcp__claude_ai_Gmail__unmark_message_spam","mcp__claude_ai_Gmail__unmark_thread_spam","mcp__claude_ai_Gmail__untrash_message","mcp__claude_ai_Gmail__untrash_thread","mcp__claude_ai_Gmail__update_draft","mcp__claude_ai_Gmail__update_label","mcp__claude_ai_Gmail__update_message_labels","mcp__fetch__fetch","mcp__godot__add_node","mcp__godot__create_scene","mcp__godot__export_mesh_library","mcp__godot__get_debug_output","mcp__godot__get_godot_version","mcp__godot__get_project_info","mcp__godot__get_uid","mcp__godot__launch_editor","mcp__godot__list_projects","mcp__godot__load_sprite","mcp__godot__run_project","mcp__godot__save_scene","mcp__godot__stop_project","mcp__godot__update_project_uids","mcp__google-workspace__batch_modify_gmail_message_labels","mcp__google-workspace__batch_update_doc","mcp__google-workspace__check_drive_file_public_access","mcp__google-workspace__copy_drive_file","mcp__google-workspace__create_calendar","mcp__google-workspace__create_doc","mcp__google-workspace__create_drive_file","mcp__google-workspace__create_drive_folder","mcp__google-workspace__create_reaction","mcp__google-workspace__create_table_with_data","mcp__google-workspace__debug_docs_runtime_info","mcp__google-workspace__debug_table_structure","mcp__google-workspace__download_chat_attachment","mcp__google-workspace__draft_gmail_message","mcp__google-workspace__export_doc_to_pdf","mcp__google-workspace__find_and_replace_doc","mcp__google-workspace__get_doc_as_markdown","mcp__google-workspace__get_doc_content","mcp__google-workspace__get_drive_file_content","mcp__google-workspace__get_drive_file_download_url","mcp__google-workspace__get_drive_file_permissions","mcp__google-workspace__get_drive_shareable_link","mcp__google-workspace__get_events","mcp__google-workspace__get_gmail_attachment_content","mcp__google-workspace__get_gmail_message_content","mcp__google-workspace__get_gmail_messages_content_batch","mcp__google-workspace__get_gmail_thread_content","mcp__google-workspace__get_gmail_threads_content_batch","mcp__google-workspace__get_messages","mcp__google-workspace__import_to_google_doc","mcp__google-workspace__import_to_google_sheets","mcp__google-workspace__import_to_google_slides","mcp__google-workspace__insert_doc_elements","mcp__google-workspace__insert_doc_image","mcp__google-workspace__inspect_doc_structure","mcp__google-workspace__list_calendars","mcp__google-workspace__list_docs_in_folder","mcp__google-workspace__list_document_comments","mcp__google-workspace__list_drive_items","mcp__google-workspace__list_gmail_filters","mcp__google-workspace__list_gmail_labels","mcp__google-workspace__list_spaces","mcp__google-workspace__manage_doc_tab","mcp__google-workspace__manage_document_comment","mcp__google-workspace__manage_drive_access","mcp__google-workspace__manage_event","mcp__google-workspace__manage_focus_time","mcp__google-workspace__manage_gmail_filter","mcp__google-workspace__manage_gmail_label","mcp__google-workspace__manage_out_of_office","mcp__google-workspace__modify_doc_text","mcp__google-workspace__modify_gmail_message_labels","mcp__google-workspace__query_freebusy","mcp__google-workspace__search_docs","mcp__google-workspace__search_drive_files","mcp__google-workspace__search_gmail_messages","mcp__google-workspace__search_messages","mcp__google-workspace__send_gmail_message","mcp__google-workspace__send_message","mcp__google-workspace__set_drive_file_permissions","mcp__google-workspace__start_google_auth","mcp__google-workspace__update_doc_headers_footers","mcp__google-workspace__update_drive_file","mcp__google-workspace__update_paragraph_style","mcp__playwright__browser_click","mcp__playwright__browser_close","mcp__playwright__browser_console_messages","mcp__playwright__browser_drag","mcp__playwright__browser_drop","mcp__playwright__browser_evaluate","mcp__playwright__browser_file_upload","mcp__playwright__browser_fill_form","mcp__playwright__browser_handle_dialog","mcp__playwright__browser_hover","mcp__playwright__browser_navigate","mcp__playwright__browser_navigate_back","mcp__playwright__browser_network_request","mcp__playwright__browser_network_requests","mcp__playwright__browser_press_key","mcp__playwright__browser_resize","mcp__playwright__browser_run_code_unsafe","mcp__playwright__browser_select_option","mcp__playwright__browser_snapshot","mcp__playwright__browser_tabs","mcp__playwright__browser_take_screenshot","mcp__playwright__browser_type","mcp__playwright__browser_wait_for","mcp__session-manager-scheduler__chat_send_prompt","mcp__session-manager-scheduler__feedback_list_projects","mcp__session-manager-scheduler__feedback_open_session","mcp__session-manager-scheduler__project_home_get_contract","mcp__session-manager-scheduler__project_home_render","mcp__session-manager-scheduler__project_home_status","mcp__session-manager-scheduler__project_home_validate_summary","mcp__session-manager-scheduler__scheduler_archive_prd","mcp__session-manager-scheduler__scheduler_cancel_job","mcp__session-manager-scheduler__scheduler_create_prd","mcp__session-manager-scheduler__scheduler_get_prd","mcp__session-manager-scheduler__scheduler_list_jobs","mcp__session-manager-scheduler__scheduler_list_prds","mcp__session-manager-scheduler__scheduler_reset_job","mcp__session-manager-scheduler__scheduler_retag_prd","mcp__session-manager-scheduler__scheduler_update_prd","mcp__session-manager-scheduler__session_manager_help"],"mcp_servers":[{"name":"plugin:stripe:stripe","status":"needs-auth"},{"name":"fetch","status":"connected"},{"name":"sqlite","status":"failed"},{"name":"playwright","status":"connected"},{"name":"google-workspace","status":"connected"},{"name":"n8n","status":"failed"},{"name":"session-manager-scheduler","status":"connected"},{"name":"bilko-host","status":"connected"},{"name":"godot","status":"connected"},{"name":"claude.ai Google Calendar","status":"needs-auth"},{"name":"claude.ai Google Drive","status":"needs-auth"},{"name":"claude.ai Gmail","status":"connected"}],"model":"claude-fable-5-1","permissionMode":"bypassPermissions","slash_commands":["refresh-claude-md","deep-research","stripe:explain-error","stripe:test-cards","session-manager-dev:blog-for-project-feature","session-manager-dev:builder","session-manager-dev:develop","session-manager-dev:discover-features","session-manager-dev:explain-to-me","session-manager-dev:find-opportunity","session-manager-dev:issue-address","session-manager-dev:local-project-health","session-manager-dev:memory-sanitation","session-manager-dev:ops-sweep","session-manager-dev:pr-review-sweep","session-manager-dev:pr-signal","session-manager-dev:project-status","session-manager-dev:requesting-code-review","session-manager-dev:send-feedback","stripe:connect-recommend","stripe:connect-required-verification-information","stripe:stripe-apps","stripe:stripe-best-practices","stripe:stripe-directory","stripe:stripe-docs","stripe:stripe-projects","stripe:upgrade-stripe","design-sync","dataviz","update-config","verify","debug","code-review","simplify","batch","fewer-permission-prompts","doctor","loop","schedule","claude-api","workflow-authoring","run","run-skill-generator","advisor","agents","auto-mode-setup","autocompact","clear","color","compact","config","context","effort","fast","heapdump","init","mcp","import","model","__remote-workflow","workflow-launch-exec","reload-plugins","reload-skills","rename","ultrareview","security-review","usage-credits","extra-usage","usage","insights","recap","skill-doctor","goal","design","design-consent","design-revoke","list-agents","team-onboarding","mcp__fetch__fetch"],"terminal_slash_commands":["doctor","color","reload-plugins"],"apiKeySource":"none","claude_code_version":"2.1.261","output_style":"default","agents":["architect","bilko-host-publisher","builder","claude","dev-lead","Explore","general-purpose","Plan","project-home-builder","statusline-setup","stripe:Company Researcher"],"skills":["deep-research","session-manager-dev:blog-for-project-feature","session-manager-dev:builder","session-manager-dev:develop","session-manager-dev:discover-features","session-manager-dev:explain-to-me","session-manager-dev:find-opportunity","session-manager-dev:issue-address","session-manager-dev:local-project-health","session-manager-dev:memory-sanitation","session-manager-dev:ops-sweep","session-manager-dev:pr-review-sweep","session-manager-dev:pr-signal","session-manager-dev:project-status","session-manager-dev:requesting-code-review","session-manager-dev:send-feedback","stripe:connect-recommend","stripe:connect-required-verification-information","stripe:stripe-apps","stripe:stripe-best-practices","stripe:stripe-directory","stripe:stripe-docs","stripe:stripe-projects","stripe:upgrade-stripe","design-sync","dataviz","update-config","verify","debug","code-review","simplify","batch","fewer-permission-prompts","doctor","loop","schedule","claude-api","workflow-authoring","run","run-skill-generator"],"plugins":[{"name":"session-manager-dev","path":"/home/bilko/Projects/session-manager/plugins/session-manager-dev","source":"session-manager-dev@session-manager","version":"0.1.0"},{"name":"stripe","path":"/home/bilko/.claude/plugins/cache/claude-plugins-official/stripe/0.6.4","source":"stripe@claude-plugins-official","version":"0.6.4"}],"capabilities":["interrupt_receipt_v1","interrupt_cancel_queued_v1","msg_lifecycle_v1"],"analytics_disabled":false,"product_feedback_disabled":false,"uuid":"7f8e3566-fc40-427c-a9ad-0c0719d7f3b2","memory_paths":{"auto":"/home/bilko/.claude/projects/-home-bilko-Projects-starry-night-ships/memory/"},"messaging_socket_path":"/run/user/1000/cc-socks/3537290.sock","fast_mode_state":"off","fast_mode_disabled_reason":"sdk_opt_in_required"}
9
+ {"type":"rate_limit_event","rate_limit_info":{"status":"rejected","resetsAt":1789059600,"rateLimitType":"seven_day_overage_included","overageStatus":"rejected","overageDisabledReason":"org_level_disabled","isUsingOverage":false,"unifiedWindows":{"five_hour":{"utilization":0,"resetsAt":1788643800},"seven_day":{"utilization":0.59,"resetsAt":1789059600},"seven_day_overage_included":{"utilization":1,"resetsAt":1789059600}}},"uuid":"faa8b505-db69-4afe-9436-9c812b76569a","session_id":"5cb9d69b-e2e7-49f9-806c-2307f1c94b2b"}
10
+ {"type":"assistant","message":{"diagnostics":null,"id":"dcbde611-72e9-4f07-bc35-c8ec9911f789","container":null,"model":"<synthetic>","role":"assistant","stop_details":null,"stop_reason":"stop_sequence","stop_sequence":"","type":"message","usage":{"output_tokens_details":null,"input_tokens":0,"output_tokens":0,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"server_tool_use":{"web_search_requests":0,"web_fetch_requests":0},"service_tier":null,"cache_creation":{"ephemeral_1h_input_tokens":0,"ephemeral_5m_input_tokens":0},"inference_geo":null,"iterations":null,"speed":null},"content":[{"type":"text","text":"You've reached your Fable limit. Switch to another model, or manage usage credits at claude.ai/settings/usage?from=cc_cli_limit_message, to continue."}],"context_management":null},"parent_tool_use_id":null,"session_id":"5cb9d69b-e2e7-49f9-806c-2307f1c94b2b","uuid":"a651974e-39c4-440f-a34e-eeec87e65fa4","timestamp":"2026-09-05T16:32:35.275Z","error":"rate_limit","request_id":"req_011CekcYKs4mrLVHXVc5xC2X","is_api_error_message":true}
11
+ {"duration_api_ms":1120,"stop_reason":"stop_sequence","session_id":"5cb9d69b-e2e7-49f9-806c-2307f1c94b2b","total_cost_usd":0.005226,"usage":{"output_tokens_details":{"thinking_tokens":0},"input_tokens":0,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":0,"server_tool_use":{"web_search_requests":0,"web_fetch_requests":0},"service_tier":"standard","cache_creation":{"ephemeral_1h_input_tokens":0,"ephemeral_5m_input_tokens":0},"inference_geo":"","iterations":[],"speed":"standard"},"modelUsage":{"claude-haiku-4-5-20251001":{"inputTokens":5161,"outputTokens":13,"cacheReadInputTokens":0,"cacheCreationInputTokens":0,"webSearchRequests":0,"costUSD":0.005226,"contextWindow":200000,"maxOutputTokens":32000,"thinkingTokens":0,"canonicalModel":"claude-haiku-4-5","provider":"firstParty","costBasis":"list"}},"permission_denials":[],"terminal_reason":"api_error","fast_mode_state":"off","fast_mode_disabled_reason":"sdk_opt_in_required","subagent_stats":{"spawned":0,"requested":{"background":0,"foreground":0,"unset":0},"started_in_background":0,"max_depth":0,"spawned_by_subagents":0,"completed":0,"failed":0,"killed":{"parent":0,"user":0,"system":0},"refused":{"depth_limit":0,"concurrency_limit":0,"budget":0},"by_type":{}},"is_error":true,"num_turns":1,"subtype":"success","api_error_status":429,"result":"You've reached your Fable limit. Switch to another model, or manage usage credits at claude.ai/settings/usage?from=cc_cli_limit_message, to continue.","type":"result","duration_ms":1036,"uuid":"b729d2fe-95bd-41a0-9558-eb7149222eb3","queued_turn_count":0}
12
+
13
+ [scheduler] exit code=1 (raw code=1 signal=null) duration=5s
@@ -13,7 +13,7 @@ const fs = require('node:fs');
13
13
  const os = require('node:os');
14
14
  const path = require('node:path');
15
15
  const { execFileSync } = require('node:child_process');
16
- const { landedSinceRun } = require('../landedSinceRun.cjs');
16
+ const { landedSinceRun, landedOnMainSince } = require('../landedSinceRun.cjs');
17
17
 
18
18
  function mkRepo() {
19
19
  const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'landed-since-run-'));
@@ -23,6 +23,16 @@ function mkRepo() {
23
23
  return dir;
24
24
  }
25
25
 
26
+ // -b main pins the branch name regardless of the host's init.defaultBranch
27
+ // config — landedOnMainSince's tests need a deterministic 'main' ref.
28
+ function mkRepoOnMain() {
29
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'landed-on-main-'));
30
+ execFileSync('git', ['-C', dir, 'init', '-q', '-b', 'main']);
31
+ execFileSync('git', ['-C', dir, 'config', 'user.email', 'test@test.com']);
32
+ execFileSync('git', ['-C', dir, 'config', 'user.name', 'Test']);
33
+ return dir;
34
+ }
35
+
26
36
  function commitFile(dir, relPath, content, message) {
27
37
  const abs = path.join(dir, relPath);
28
38
  fs.mkdirSync(path.dirname(abs), { recursive: true });
@@ -71,3 +81,53 @@ test('has a bounded default timeout', () => {
71
81
  const { LANDED_SINCE_RUN_TIMEOUT_MS } = require('../landedSinceRun.cjs');
72
82
  assert.ok(LANDED_SINCE_RUN_TIMEOUT_MS > 0 && LANDED_SINCE_RUN_TIMEOUT_MS <= 60_000);
73
83
  });
84
+
85
+ // landedOnMainSince — PRD 1136's already-satisfied-on-main evidence query:
86
+ // scoped to `main` (not --all) and to the PRD's declared paths.
87
+
88
+ test('landedOnMainSince: finds a commit on main after sinceIso touching a declared path', async () => {
89
+ const dir = mkRepoOnMain();
90
+ commitFile(dir, 'README.md', 'hello', 'initial');
91
+ const since = new Date().toISOString();
92
+ await new Promise((r) => setTimeout(r, 1100));
93
+ commitFile(dir, 'src/foo.js', 'hello', 'fix foo');
94
+ const shas = await landedOnMainSince(dir, since, ['src/foo.js']);
95
+ assert.strictEqual(shas.length, 1);
96
+ });
97
+
98
+ test('landedOnMainSince: ignores a commit that does not touch any declared path', async () => {
99
+ const dir = mkRepoOnMain();
100
+ commitFile(dir, 'README.md', 'hello', 'initial');
101
+ const since = new Date().toISOString();
102
+ await new Promise((r) => setTimeout(r, 1100));
103
+ commitFile(dir, 'src/unrelated.js', 'hello', 'touch unrelated');
104
+ const shas = await landedOnMainSince(dir, since, ['src/foo.js']);
105
+ assert.deepStrictEqual(shas, []);
106
+ });
107
+
108
+ test('landedOnMainSince: a commit older than sinceIso is not evidence', async () => {
109
+ const dir = mkRepoOnMain();
110
+ commitFile(dir, 'src/foo.js', 'hello', 'fix foo');
111
+ await new Promise((r) => setTimeout(r, 1100));
112
+ const since = new Date().toISOString();
113
+ const shas = await landedOnMainSince(dir, since, ['src/foo.js']);
114
+ assert.deepStrictEqual(shas, []);
115
+ });
116
+
117
+ test('landedOnMainSince: empty paths never fabricates evidence — resolves []', async () => {
118
+ const dir = mkRepoOnMain();
119
+ commitFile(dir, 'src/foo.js', 'hello', 'fix foo');
120
+ const shas = await landedOnMainSince(dir, new Date(0).toISOString(), []);
121
+ assert.deepStrictEqual(shas, []);
122
+ });
123
+
124
+ test('landedOnMainSince: no cwd resolves [] without throwing', async () => {
125
+ const shas = await landedOnMainSince(null, new Date().toISOString(), ['src/foo.js']);
126
+ assert.deepStrictEqual(shas, []);
127
+ });
128
+
129
+ test('landedOnMainSince: git-unavailable (non-repo cwd) resolves [] without throwing', async () => {
130
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'landed-on-main-norepo-'));
131
+ const shas = await landedOnMainSince(dir, new Date().toISOString(), ['src/foo.js']);
132
+ assert.deepStrictEqual(shas, []);
133
+ });
@@ -0,0 +1,88 @@
1
+ /**
2
+ * rateLimitWindow.test.cjs — resolveBindingRateLimitReset: reads the log's
3
+ * OWN rate_limit_event windows and returns the reset of whichever window
4
+ * actually bound (utilization >= 1.0), not the billing endpoint's five_hour
5
+ * assumption (PRD 1118).
6
+ *
7
+ * Run: timeout 120 npx vitest run src/main/lib/__tests__/rateLimitWindow.test.cjs
8
+ */
9
+
10
+ 'use strict';
11
+
12
+ import { test } from 'vitest';
13
+ const assert = require('node:assert/strict');
14
+ const fs = require('node:fs');
15
+ const os = require('node:os');
16
+ const path = require('node:path');
17
+ const { resolveBindingRateLimitReset } = require('../rateLimitWindow.cjs');
18
+
19
+ // Repo-tracked, byte-for-byte copy of the real incident log (2026-09-05
20
+ // starry-night-ships 429) so this test is hermetic — CI, a fresh clone, or
21
+ // this machine's own run-log retention sweep (runLogRetention.test.cjs)
22
+ // pruning the live copy must not turn this red for reasons unrelated to the
23
+ // code under test.
24
+ const TRACKED_FIXTURE = path.join(__dirname, 'fixtures', '204-mercury-steam-horse.log.txt');
25
+ const REAL_FIXTURE_LIVE_PATH = '/home/bilko/.claude/session-manager/scheduled-plans/runs/2026-09-05T16-32-18-381Z/204-mercury-steam-horse.log';
26
+
27
+ test('tracked fixture (verbatim copy of the real incident log): seven_day_overage_included (utilization 1.0) binds, not five_hour', () => {
28
+ const reset = resolveBindingRateLimitReset(TRACKED_FIXTURE);
29
+ assert.strictEqual(reset, 1789059600);
30
+ assert.notStrictEqual(reset, 1788643800);
31
+ });
32
+
33
+ test('tracked fixture is byte-identical to the live incident log, when present on this machine', () => {
34
+ if (!fs.existsSync(REAL_FIXTURE_LIVE_PATH)) return; // machine-local artifact, not guaranteed to exist
35
+ assert.strictEqual(
36
+ fs.readFileSync(TRACKED_FIXTURE, 'utf8'),
37
+ fs.readFileSync(REAL_FIXTURE_LIVE_PATH, 'utf8'),
38
+ );
39
+ });
40
+
41
+ function writeTmpLog(lines) {
42
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'rate-limit-window-test-'));
43
+ const file = path.join(dir, 'job.log');
44
+ fs.writeFileSync(file, lines.map((l) => (typeof l === 'string' ? l : JSON.stringify(l))).join('\n') + '\n');
45
+ return file;
46
+ }
47
+
48
+ test('no rate_limit_event in log → null', () => {
49
+ const logPath = writeTmpLog(['[scheduler] starting job', '{"type":"system","subtype":"init"}']);
50
+ assert.strictEqual(resolveBindingRateLimitReset(logPath), null);
51
+ });
52
+
53
+ test('rate_limit_event present but no window at/above 1.0 utilization → null', () => {
54
+ const logPath = writeTmpLog([
55
+ { type: 'rate_limit_event', rate_limit_info: { unifiedWindows: {
56
+ five_hour: { utilization: 0, resetsAt: 1788643800 },
57
+ seven_day: { utilization: 0.59, resetsAt: 1789059600 },
58
+ } } },
59
+ ]);
60
+ assert.strictEqual(resolveBindingRateLimitReset(logPath), null);
61
+ });
62
+
63
+ test('multiple windows bind in the last event → the LATEST reset wins', () => {
64
+ const logPath = writeTmpLog([
65
+ { type: 'rate_limit_event', rate_limit_info: { unifiedWindows: {
66
+ five_hour: { utilization: 1.0, resetsAt: 1000 },
67
+ seven_day: { utilization: 1.0, resetsAt: 2000 },
68
+ } } },
69
+ ]);
70
+ assert.strictEqual(resolveBindingRateLimitReset(logPath), 2000);
71
+ });
72
+
73
+ test('uses the LAST rate_limit_event in the log, not the first', () => {
74
+ const logPath = writeTmpLog([
75
+ { type: 'rate_limit_event', rate_limit_info: { unifiedWindows: {
76
+ seven_day: { utilization: 0.3, resetsAt: 111 },
77
+ } } },
78
+ { type: 'other' },
79
+ { type: 'rate_limit_event', rate_limit_info: { unifiedWindows: {
80
+ seven_day_overage_included: { utilization: 1.0, resetsAt: 999 },
81
+ } } },
82
+ ]);
83
+ assert.strictEqual(resolveBindingRateLimitReset(logPath), 999);
84
+ });
85
+
86
+ test('missing file → null (best-effort, matches readTail contract)', () => {
87
+ assert.strictEqual(resolveBindingRateLimitReset('/nonexistent/path/job.log'), null);
88
+ });