@rubytech/create-maxy-code 0.1.459 → 0.1.461
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/__tests__/cron-heartbeat-registration.test.js +57 -28
- package/dist/__tests__/launchd-plist.test.js +26 -0
- package/dist/cron-registration.js +30 -8
- package/dist/index.js +50 -9
- package/dist/launchd-plist.js +1 -1
- package/dist/uninstall.js +9 -3
- package/package.json +1 -1
- package/payload/platform/lib/storage-broker/dist/__tests__/cf-exec.test.js +116 -6
- package/payload/platform/lib/storage-broker/dist/__tests__/cf-exec.test.js.map +1 -1
- package/payload/platform/lib/storage-broker/dist/__tests__/object-limits.test.js +20 -0
- package/payload/platform/lib/storage-broker/dist/__tests__/object-limits.test.js.map +1 -1
- package/payload/platform/lib/storage-broker/dist/__tests__/resource-limits.test.d.ts +2 -0
- package/payload/platform/lib/storage-broker/dist/__tests__/resource-limits.test.d.ts.map +1 -0
- package/payload/platform/lib/storage-broker/dist/__tests__/resource-limits.test.js +41 -0
- package/payload/platform/lib/storage-broker/dist/__tests__/resource-limits.test.js.map +1 -0
- package/payload/platform/lib/storage-broker/dist/cf-exec.d.ts +1 -0
- package/payload/platform/lib/storage-broker/dist/cf-exec.d.ts.map +1 -1
- package/payload/platform/lib/storage-broker/dist/cf-exec.js +116 -48
- package/payload/platform/lib/storage-broker/dist/cf-exec.js.map +1 -1
- package/payload/platform/lib/storage-broker/dist/index.d.ts +1 -0
- package/payload/platform/lib/storage-broker/dist/index.d.ts.map +1 -1
- package/payload/platform/lib/storage-broker/dist/index.js +1 -0
- package/payload/platform/lib/storage-broker/dist/index.js.map +1 -1
- package/payload/platform/lib/storage-broker/dist/object-limits.d.ts +27 -0
- package/payload/platform/lib/storage-broker/dist/object-limits.d.ts.map +1 -1
- package/payload/platform/lib/storage-broker/dist/object-limits.js +27 -7
- package/payload/platform/lib/storage-broker/dist/object-limits.js.map +1 -1
- package/payload/platform/lib/storage-broker/dist/resource-limits.d.ts +102 -0
- package/payload/platform/lib/storage-broker/dist/resource-limits.d.ts.map +1 -0
- package/payload/platform/lib/storage-broker/dist/resource-limits.js +105 -0
- package/payload/platform/lib/storage-broker/dist/resource-limits.js.map +1 -0
- package/payload/platform/lib/storage-broker/src/__tests__/cf-exec.test.ts +130 -7
- package/payload/platform/lib/storage-broker/src/__tests__/object-limits.test.ts +30 -0
- package/payload/platform/lib/storage-broker/src/__tests__/resource-limits.test.ts +49 -0
- package/payload/platform/lib/storage-broker/src/cf-exec.ts +129 -56
- package/payload/platform/lib/storage-broker/src/index.ts +1 -0
- package/payload/platform/lib/storage-broker/src/object-limits.ts +26 -6
- package/payload/platform/lib/storage-broker/src/resource-limits.ts +105 -0
- package/payload/platform/plugins/admin/skills/platform-architecture/SKILL.md +5 -4
- package/payload/platform/plugins/cloudflare/PLUGIN.md +3 -1
- package/payload/platform/plugins/cloudflare/bin/portal-enrol.mjs +96 -0
- package/payload/platform/plugins/cloudflare/mcp/__tests__/auth-route.test.ts +220 -0
- package/payload/platform/plugins/cloudflare/mcp/__tests__/authorize.test.ts +201 -0
- package/payload/platform/plugins/cloudflare/mcp/__tests__/file-routes.test.ts +178 -0
- package/payload/platform/plugins/cloudflare/mcp/__tests__/harness.test.ts +32 -0
- package/payload/platform/plugins/cloudflare/mcp/__tests__/passcode.test.ts +204 -0
- package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-enrol.test.ts +207 -0
- package/payload/platform/plugins/cloudflare/mcp/__tests__/ratelimit.test.ts +77 -0
- package/payload/platform/plugins/cloudflare/mcp/__tests__/skill-contract.test.ts +46 -0
- package/payload/platform/plugins/cloudflare/mcp/__tests__/template-config.test.ts +55 -0
- package/payload/platform/plugins/cloudflare/mcp/__tests__/upload-route.test.ts +255 -0
- package/payload/platform/plugins/cloudflare/mcp/package.json +5 -2
- package/payload/platform/plugins/cloudflare/references/r2-object-storage.md +46 -0
- package/payload/platform/plugins/cloudflare/skills/data-portal/SKILL.md +125 -0
- package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/_lib/authorize.ts +32 -0
- package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/_lib/log.ts +12 -0
- package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/_lib/passcode.mjs +193 -0
- package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/_lib/ratelimit.ts +55 -0
- package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/_lib/session.ts +85 -0
- package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/_lib/types.ts +44 -0
- package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/auth.ts +119 -0
- package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/delete.ts +89 -0
- package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/download.ts +70 -0
- package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/files.ts +41 -0
- package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/upload.ts +151 -0
- package/payload/platform/plugins/cloudflare/skills/data-portal/template/public/index.html +42 -0
- package/payload/platform/plugins/cloudflare/skills/data-portal/template/public/portal.css +113 -0
- package/payload/platform/plugins/cloudflare/skills/data-portal/template/public/portal.js +155 -0
- package/payload/platform/plugins/cloudflare/skills/data-portal/template/schema.sql +73 -0
- package/payload/platform/plugins/cloudflare/skills/data-portal/template/wrangler.toml +21 -0
- package/payload/platform/plugins/cloudflare/skills/site-deploy/SKILL.md +8 -0
- package/payload/platform/plugins/docs/references/outlook-guide.md +3 -3
- package/payload/platform/plugins/docs/references/voice-mirror-guide.md +1 -0
- package/payload/platform/plugins/filesystem/PLUGIN.md +0 -1
- package/payload/platform/plugins/filesystem/mcp/dist/lib/scope.d.ts +14 -10
- package/payload/platform/plugins/filesystem/mcp/dist/lib/scope.d.ts.map +1 -1
- package/payload/platform/plugins/filesystem/mcp/dist/lib/scope.js +14 -10
- package/payload/platform/plugins/filesystem/mcp/dist/lib/scope.js.map +1 -1
- package/payload/platform/plugins/outlook/.claude-plugin/plugin.json +1 -1
- package/payload/platform/plugins/outlook/PLUGIN.md +3 -5
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/detach.test.d.ts +2 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/detach.test.d.ts.map +1 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/detach.test.js +75 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/detach.test.js.map +1 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-edit-removal.test.d.ts +2 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-edit-removal.test.d.ts.map +1 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-edit-removal.test.js +187 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-edit-removal.test.js.map +1 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-edit.test.js +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-edit.test.js.map +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/index.js +3 -2
- package/payload/platform/plugins/outlook/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/lib/attachment-list.d.ts +43 -0
- package/payload/platform/plugins/outlook/mcp/dist/lib/attachment-list.d.ts.map +1 -0
- package/payload/platform/plugins/outlook/mcp/dist/lib/attachment-list.js +65 -0
- package/payload/platform/plugins/outlook/mcp/dist/lib/attachment-list.js.map +1 -0
- package/payload/platform/plugins/outlook/mcp/dist/lib/detach.d.ts +65 -0
- package/payload/platform/plugins/outlook/mcp/dist/lib/detach.d.ts.map +1 -0
- package/payload/platform/plugins/outlook/mcp/dist/lib/detach.js +99 -0
- package/payload/platform/plugins/outlook/mcp/dist/lib/detach.js.map +1 -0
- package/payload/platform/plugins/outlook/mcp/dist/lib/outbound-attachments.d.ts +6 -1
- package/payload/platform/plugins/outlook/mcp/dist/lib/outbound-attachments.d.ts.map +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/lib/outbound-attachments.js.map +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/tools/draft-edit.d.ts +42 -22
- package/payload/platform/plugins/outlook/mcp/dist/tools/draft-edit.d.ts.map +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/tools/draft-edit.js +59 -23
- package/payload/platform/plugins/outlook/mcp/dist/tools/draft-edit.js.map +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/tools/mail-attachment.d.ts +15 -16
- package/payload/platform/plugins/outlook/mcp/dist/tools/mail-attachment.d.ts.map +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/tools/mail-attachment.js +8 -41
- package/payload/platform/plugins/outlook/mcp/dist/tools/mail-attachment.js.map +1 -1
- package/payload/platform/plugins/outlook/references/auth.md +1 -1
- package/payload/platform/plugins/outlook/references/graph-surfaces.md +7 -3
- package/payload/platform/plugins/outlook/skills/outlook/SKILL.md +1 -1
- package/payload/platform/plugins/storage-broker/PLUGIN.md +1 -1
- package/payload/platform/plugins/voice-mirror/PLUGIN.md +19 -7
- package/payload/platform/plugins/voice-mirror/mcp/dist/index.d.ts.map +1 -1
- package/payload/platform/plugins/voice-mirror/mcp/dist/index.js +35 -16
- package/payload/platform/plugins/voice-mirror/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/voice-mirror/mcp/dist/lib/admin-user.d.ts +31 -0
- package/payload/platform/plugins/voice-mirror/mcp/dist/lib/admin-user.d.ts.map +1 -0
- package/payload/platform/plugins/voice-mirror/mcp/dist/lib/admin-user.js +87 -0
- package/payload/platform/plugins/voice-mirror/mcp/dist/lib/admin-user.js.map +1 -0
- package/payload/platform/plugins/voice-mirror/mcp/dist/tools/voice-distil-profile.d.ts.map +1 -1
- package/payload/platform/plugins/voice-mirror/mcp/dist/tools/voice-distil-profile.js +111 -12
- package/payload/platform/plugins/voice-mirror/mcp/dist/tools/voice-distil-profile.js.map +1 -1
- package/payload/platform/plugins/voice-mirror/mcp/dist/tools/voice-ingest-session-text.d.ts.map +1 -1
- package/payload/platform/plugins/voice-mirror/mcp/dist/tools/voice-ingest-session-text.js +2 -1
- package/payload/platform/plugins/voice-mirror/mcp/dist/tools/voice-ingest-session-text.js.map +1 -1
- package/payload/platform/plugins/voice-mirror/mcp/dist/tools/voice-record-feedback.d.ts.map +1 -1
- package/payload/platform/plugins/voice-mirror/mcp/dist/tools/voice-record-feedback.js +2 -1
- package/payload/platform/plugins/voice-mirror/mcp/dist/tools/voice-record-feedback.js.map +1 -1
- package/payload/platform/plugins/voice-mirror/mcp/dist/tools/voice-retrieve-conditioning.d.ts.map +1 -1
- package/payload/platform/plugins/voice-mirror/mcp/dist/tools/voice-retrieve-conditioning.js +54 -21
- package/payload/platform/plugins/voice-mirror/mcp/dist/tools/voice-retrieve-conditioning.js.map +1 -1
- package/payload/platform/plugins/voice-mirror/mcp/dist/tools/voice-tag-content.d.ts +2 -2
- package/payload/platform/plugins/voice-mirror/mcp/dist/tools/voice-tag-content.d.ts.map +1 -1
- package/payload/platform/plugins/voice-mirror/mcp/dist/tools/voice-tag-content.js +12 -3
- package/payload/platform/plugins/voice-mirror/mcp/dist/tools/voice-tag-content.js.map +1 -1
- package/payload/platform/plugins/voice-mirror/mcp/scripts/smoke.mjs +444 -0
- package/payload/platform/plugins/voice-mirror/skills/voice-mirror/SKILL.md +18 -2
- package/payload/platform/scripts/__tests__/logs-rotate.test.sh +256 -0
- package/payload/platform/scripts/__tests__/resume-tunnel.test.sh +79 -0
- package/payload/platform/scripts/logs-rotate.sh +204 -0
- package/payload/platform/scripts/resume-tunnel.sh +27 -0
- package/payload/platform/scripts/voice-author-key-audit.sh +155 -0
- package/payload/platform/services/claude-session-manager/dist/http-server.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/http-server.js +41 -5
- package/payload/platform/services/claude-session-manager/dist/http-server.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/index.d.ts +1 -0
- package/payload/platform/services/claude-session-manager/dist/index.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/index.js +22 -0
- package/payload/platform/services/claude-session-manager/dist/index.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/install-start-counter.d.ts +2 -0
- package/payload/platform/services/claude-session-manager/dist/install-start-counter.d.ts.map +1 -0
- package/payload/platform/services/claude-session-manager/dist/install-start-counter.js +12 -0
- package/payload/platform/services/claude-session-manager/dist/install-start-counter.js.map +1 -0
- package/payload/platform/services/claude-session-manager/dist/mcp-descriptor-resolve.d.ts +17 -0
- package/payload/platform/services/claude-session-manager/dist/mcp-descriptor-resolve.d.ts.map +1 -0
- package/payload/platform/services/claude-session-manager/dist/mcp-descriptor-resolve.js +88 -0
- package/payload/platform/services/claude-session-manager/dist/mcp-descriptor-resolve.js.map +1 -0
- package/payload/platform/services/claude-session-manager/dist/pty-census.d.ts +122 -9
- package/payload/platform/services/claude-session-manager/dist/pty-census.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/pty-census.js +214 -29
- package/payload/platform/services/claude-session-manager/dist/pty-census.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/pty-spawner.d.ts +0 -1
- package/payload/platform/services/claude-session-manager/dist/pty-spawner.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/pty-spawner.js +36 -24
- package/payload/platform/services/claude-session-manager/dist/pty-spawner.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/rc-daemon.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/rc-daemon.js +13 -1
- package/payload/platform/services/claude-session-manager/dist/rc-daemon.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/session-memory-cap.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/session-memory-cap.js +4 -2
- package/payload/platform/services/claude-session-manager/dist/session-memory-cap.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/slice-memory-policy.d.ts +150 -0
- package/payload/platform/services/claude-session-manager/dist/slice-memory-policy.d.ts.map +1 -0
- package/payload/platform/services/claude-session-manager/dist/slice-memory-policy.js +333 -0
- package/payload/platform/services/claude-session-manager/dist/slice-memory-policy.js.map +1 -0
- package/payload/platform/services/claude-session-manager/dist/start-counter.d.ts +10 -0
- package/payload/platform/services/claude-session-manager/dist/start-counter.d.ts.map +1 -0
- package/payload/platform/services/claude-session-manager/dist/start-counter.js +55 -0
- package/payload/platform/services/claude-session-manager/dist/start-counter.js.map +1 -0
- package/payload/server/{chunk-Q6W4U6HL.js → chunk-JXWFVE5X.js} +83 -39
- package/payload/server/server.js +268 -73
- package/payload/server/{src-3I2RYZFB.js → src-4F37OHLK.js} +11 -1
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/pkce-flow.test.d.ts +0 -2
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/pkce-flow.test.js +0 -215
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/pkce-flow.test.js.map +0 -1
|
@@ -1,14 +1,15 @@
|
|
|
1
|
-
// Acceptance gate for the
|
|
1
|
+
// Acceptance gate for the per-brand cron block.
|
|
2
2
|
//
|
|
3
|
-
// The installer registers exactly
|
|
4
|
-
// check-due-events.js once a minute
|
|
5
|
-
//
|
|
3
|
+
// The installer registers exactly TWO install-level cron lines: the
|
|
4
|
+
// scheduling-watcher heartbeat ticking check-due-events.js once a minute, and
|
|
5
|
+
// (Task 1706) the log rotator every 15 minutes. These invariants protect the
|
|
6
|
+
// two regressions that the original block caused (and that its narrowed
|
|
6
7
|
// replacement must not reintroduce), plus the marker contract the admin
|
|
7
8
|
// `system-status` check reads back.
|
|
8
9
|
//
|
|
9
|
-
// (a) The block carries
|
|
10
|
-
// email-auto-respond line. Those dispatchers act without an
|
|
11
|
-
// booking and stay unscheduled by the installer.
|
|
10
|
+
// (a) The block carries the heartbeat and the rotator and nothing else — no
|
|
11
|
+
// email-fetch / email-auto-respond line. Those dispatchers act without an
|
|
12
|
+
// operator booking and stay unscheduled by the installer.
|
|
12
13
|
// (b) The entry never `mkdir`s an account log dir. The legacy line ran
|
|
13
14
|
// `mkdir -p data/accounts/<id>/logs` every 60s, tripping
|
|
14
15
|
// setup-account/seed-neo4j's stub-account-dirs guard on reinstall.
|
|
@@ -22,7 +23,7 @@
|
|
|
22
23
|
// never stacks a second copy.
|
|
23
24
|
import test from "node:test";
|
|
24
25
|
import assert from "node:assert/strict";
|
|
25
|
-
import { cronBlockMarkers,
|
|
26
|
+
import { cronBlockMarkers, buildBrandCronBlock, stripCronBlock, mergeBrandCronBlock, } from "../cron-registration.js";
|
|
26
27
|
const OPTS = {
|
|
27
28
|
productName: "maxy-code",
|
|
28
29
|
platformRoot: "/home/me/maxy-code/platform",
|
|
@@ -32,7 +33,7 @@ const OPTS = {
|
|
|
32
33
|
uiPort: 19201,
|
|
33
34
|
};
|
|
34
35
|
test("heartbeat entry carries MAXY_UI_INTERNAL_PORT so the dispatcher can reach the inject route (Task 1378)", () => {
|
|
35
|
-
const block =
|
|
36
|
+
const block = buildBrandCronBlock(OPTS);
|
|
36
37
|
assert.match(block, /MAXY_UI_INTERNAL_PORT=19201 /, "entry must export the UI internal port for schedule-inject");
|
|
37
38
|
});
|
|
38
39
|
test("markers are # BEGIN/END <PRODUCT_UPPER> CRONS (admin system-status contract)", () => {
|
|
@@ -40,41 +41,69 @@ test("markers are # BEGIN/END <PRODUCT_UPPER> CRONS (admin system-status contrac
|
|
|
40
41
|
assert.equal(begin, "# BEGIN MAXY-CODE CRONS");
|
|
41
42
|
assert.equal(end, "# END MAXY-CODE CRONS");
|
|
42
43
|
});
|
|
43
|
-
test("block has exactly
|
|
44
|
-
const block =
|
|
44
|
+
test("block has exactly two entries — heartbeat and logs-rotate, no email lines", () => {
|
|
45
|
+
const block = buildBrandCronBlock(OPTS);
|
|
45
46
|
const entries = block.split("\n").filter((l) => l.trim() && !l.trim().startsWith("#"));
|
|
46
|
-
assert.equal(entries.length,
|
|
47
|
-
assert.match(entries[0], / # heartbeat$/, "entry must be tagged # heartbeat");
|
|
47
|
+
assert.equal(entries.length, 2, "expected exactly two cron entries");
|
|
48
|
+
assert.match(entries[0], / # heartbeat$/, "first entry must be tagged # heartbeat");
|
|
49
|
+
assert.match(entries[1], / # logs-rotate$/, "second entry must be tagged # logs-rotate");
|
|
48
50
|
assert.doesNotMatch(block, /email-fetch/, "email-fetch must not be scheduled");
|
|
49
51
|
assert.doesNotMatch(block, /email-auto-respond/, "email-auto-respond must not be scheduled");
|
|
50
52
|
});
|
|
51
53
|
test("heartbeat entry never mkdirs an account log dir (Task 039 guard)", () => {
|
|
52
|
-
const block =
|
|
54
|
+
const block = buildBrandCronBlock(OPTS);
|
|
53
55
|
assert.doesNotMatch(block, /mkdir/, "no mkdir in the cron line");
|
|
54
56
|
assert.doesNotMatch(block, /data\/accounts/, "must not touch the seed-guard account dir");
|
|
55
57
|
assert.match(block, />> \/home\/me\/\.maxy-code\/logs\/check-due-events\.log 2>&1/, "log must redirect to <persistDir>/logs");
|
|
56
58
|
});
|
|
57
59
|
test("NEO4J_URI is pinned to the brand bolt port, never defaulting 7687 (Task 571 guard)", () => {
|
|
58
|
-
const block =
|
|
60
|
+
const block = buildBrandCronBlock(OPTS);
|
|
59
61
|
assert.match(block, /NEO4J_URI=bolt:\/\/localhost:17687 /, "NEO4J_URI must pin the brand port");
|
|
60
62
|
assert.doesNotMatch(block, /7687(?!.)/, "must not reference the default 7687 port");
|
|
61
63
|
});
|
|
62
64
|
test("entry runs check-due-events.js with PLATFORM_ROOT and no ACCOUNT_ID", () => {
|
|
63
|
-
const block =
|
|
65
|
+
const block = buildBrandCronBlock(OPTS);
|
|
64
66
|
assert.match(block, /PLATFORM_ROOT=\/home\/me\/maxy-code\/platform /);
|
|
65
67
|
assert.match(block, /\/plugins\/scheduling\/mcp\/dist\/scripts\/check-due-events\.js/);
|
|
66
68
|
assert.doesNotMatch(block, /ACCOUNT_ID=/, "watcher scans all accounts itself; no per-account scoping");
|
|
67
69
|
});
|
|
68
70
|
test("entry fires every minute", () => {
|
|
69
|
-
const block =
|
|
71
|
+
const block = buildBrandCronBlock(OPTS);
|
|
70
72
|
const entry = block.split("\n").find((l) => l.includes("check-due-events"));
|
|
71
73
|
assert.match(entry, /^\* \* \* \* \* /, "must be a once-a-minute schedule");
|
|
72
74
|
});
|
|
75
|
+
test("Task 1706 — rotator entry ticks every 15 minutes with the logs dir as its argument", () => {
|
|
76
|
+
const block = buildBrandCronBlock(OPTS);
|
|
77
|
+
const entry = block.split("\n").find((l) => l.includes("logs-rotate.sh"));
|
|
78
|
+
assert.match(entry, /^\*\/15 \* \* \* \* /, "must be a quarter-hourly schedule");
|
|
79
|
+
assert.match(entry, /\/home\/me\/maxy-code\/platform\/scripts\/logs-rotate\.sh \/home\/me\/\.maxy-code\/logs /, "the logs dir must be passed explicitly — a co-resident brand must never rotate a sibling's logs");
|
|
80
|
+
});
|
|
81
|
+
test("Task 1706 — the rotator's own lines go to server.log, the file it bounds", () => {
|
|
82
|
+
const block = buildBrandCronBlock(OPTS);
|
|
83
|
+
const entry = block.split("\n").find((l) => l.includes("logs-rotate.sh"));
|
|
84
|
+
assert.match(entry, />> \/home\/me\/\.maxy-code\/logs\/server\.log 2>&1/);
|
|
85
|
+
});
|
|
86
|
+
test("Task 1706 — rotator entry inherits the heartbeat's guards: no mkdir, no account dir, no env", () => {
|
|
87
|
+
const block = buildBrandCronBlock(OPTS);
|
|
88
|
+
const entry = block.split("\n").find((l) => l.includes("logs-rotate.sh"));
|
|
89
|
+
assert.doesNotMatch(entry, /mkdir/, "no mkdir in the cron line (Task 039 guard)");
|
|
90
|
+
assert.doesNotMatch(entry, /data\/accounts/, "must not touch the seed-guard account dir");
|
|
91
|
+
assert.doesNotMatch(entry, /NEO4J_URI=/, "the rotator reads no env — its logs dir is an argument");
|
|
92
|
+
});
|
|
93
|
+
test("Task 1706 — system-status can name both entries from their trailing comments", () => {
|
|
94
|
+
// platform/plugins/admin/mcp/src/index.ts:466 matches /#\s*(.+)$/ per entry.
|
|
95
|
+
const block = buildBrandCronBlock(OPTS);
|
|
96
|
+
const names = block
|
|
97
|
+
.split("\n")
|
|
98
|
+
.filter((l) => l.trim() && !l.trim().startsWith("#"))
|
|
99
|
+
.map((l) => l.match(/#\s*(.+)$/)?.[1].trim());
|
|
100
|
+
assert.deepEqual(names, ["heartbeat", "logs-rotate"]);
|
|
101
|
+
});
|
|
73
102
|
test("stripCronBlock removes a present block and counts its entries", () => {
|
|
74
|
-
const tab = `MAILTO=""\n${
|
|
103
|
+
const tab = `MAILTO=""\n${buildBrandCronBlock(OPTS)}\n`;
|
|
75
104
|
const { cleaned, removedEntries, matched } = stripCronBlock(tab, "maxy-code");
|
|
76
105
|
assert.equal(matched, true);
|
|
77
|
-
assert.equal(removedEntries,
|
|
106
|
+
assert.equal(removedEntries, 2);
|
|
78
107
|
assert.equal(cleaned, 'MAILTO=""');
|
|
79
108
|
});
|
|
80
109
|
test("stripCronBlock is a no-op signal when no block is present", () => {
|
|
@@ -85,20 +114,20 @@ test("stripCronBlock is a no-op signal when no block is present", () => {
|
|
|
85
114
|
assert.equal(cleaned, "0 3 * * * /usr/bin/backup.sh");
|
|
86
115
|
});
|
|
87
116
|
test("stripCronBlock escapes the brand name (hyphen/dot safe)", () => {
|
|
88
|
-
const block =
|
|
117
|
+
const block = buildBrandCronBlock({ ...OPTS, productName: "real.agent-code" });
|
|
89
118
|
const { matched, removedEntries } = stripCronBlock(block, "real.agent-code");
|
|
90
119
|
assert.equal(matched, true);
|
|
91
|
-
assert.equal(removedEntries,
|
|
120
|
+
assert.equal(removedEntries, 2);
|
|
92
121
|
});
|
|
93
|
-
test("
|
|
94
|
-
const block =
|
|
95
|
-
const merged =
|
|
122
|
+
test("mergeBrandCronBlock appends to an empty crontab", () => {
|
|
123
|
+
const block = buildBrandCronBlock(OPTS);
|
|
124
|
+
const merged = mergeBrandCronBlock("", block, "maxy-code");
|
|
96
125
|
assert.equal(merged, `${block}\n`);
|
|
97
126
|
});
|
|
98
|
-
test("
|
|
99
|
-
const block =
|
|
100
|
-
const once =
|
|
101
|
-
const twice =
|
|
127
|
+
test("mergeBrandCronBlock replaces an existing block — idempotent re-install", () => {
|
|
128
|
+
const block = buildBrandCronBlock(OPTS);
|
|
129
|
+
const once = mergeBrandCronBlock(`0 3 * * * /usr/bin/backup.sh\n`, block, "maxy-code");
|
|
130
|
+
const twice = mergeBrandCronBlock(once, block, "maxy-code");
|
|
102
131
|
assert.equal(once, twice, "re-applying the merge must not stack a second block");
|
|
103
132
|
const blockCount = (twice.match(/# BEGIN MAXY-CODE CRONS/g) || []).length;
|
|
104
133
|
assert.equal(blockCount, 1, "exactly one brand block after re-install");
|
|
@@ -280,3 +280,29 @@ test("session-manager plist renders bash wrapper as sole ProgramArguments", () =
|
|
|
280
280
|
assert.match(xml, /<key>KeepAlive<\/key>\s*<true\/>/);
|
|
281
281
|
assert.match(xml, /<key>RunAtLoad<\/key>\s*<true\/>/);
|
|
282
282
|
});
|
|
283
|
+
test("Task 1706 — logs-rotate plist is a 15-minute one-shot taking the logs dir as an argument", () => {
|
|
284
|
+
const xml = renderPlist({
|
|
285
|
+
label: "com.rubytech.maxy-code-logs-rotate",
|
|
286
|
+
programArguments: [
|
|
287
|
+
"/Users/x/maxy-code/platform/scripts/logs-rotate.sh",
|
|
288
|
+
"/Users/x/.maxy-code/logs",
|
|
289
|
+
],
|
|
290
|
+
stdoutPath: "/Users/x/.maxy-code/logs/server.log",
|
|
291
|
+
stderrPath: "/Users/x/.maxy-code/logs/server.log",
|
|
292
|
+
keepAlive: false,
|
|
293
|
+
runAtLoad: false,
|
|
294
|
+
startInterval: 900,
|
|
295
|
+
});
|
|
296
|
+
assert.match(xml, /<string>com\.rubytech\.maxy-code-logs-rotate<\/string>/);
|
|
297
|
+
// The logs dir is passed explicitly — a co-resident brand must never rotate a
|
|
298
|
+
// sibling's logs.
|
|
299
|
+
assert.match(xml, /<string>\/Users\/x\/maxy-code\/platform\/scripts\/logs-rotate\.sh<\/string>\s*<string>\/Users\/x\/\.maxy-code\/logs<\/string>/);
|
|
300
|
+
assert.match(xml, /<key>StartInterval<\/key>\s*<integer>900<\/integer>/);
|
|
301
|
+
// A periodic one-shot, not a supervised daemon — same shape as the heartbeat.
|
|
302
|
+
assert.match(xml, /<key>KeepAlive<\/key>\s*<false\/>/);
|
|
303
|
+
assert.match(xml, /<key>RunAtLoad<\/key>\s*<false\/>/);
|
|
304
|
+
// No wrapper: unlike the heartbeat the rotator sources no .env.
|
|
305
|
+
assert.doesNotMatch(xml, /\/bin\/bash/);
|
|
306
|
+
// The rotator's own lines go to server.log, the file it itself bounds.
|
|
307
|
+
assert.match(xml, /<key>StandardOutPath<\/key>\s*<string>\/Users\/x\/\.maxy-code\/logs\/server\.log<\/string>/);
|
|
308
|
+
});
|
|
@@ -1,11 +1,21 @@
|
|
|
1
|
-
//
|
|
1
|
+
// Per-brand cron block — pure string builders.
|
|
2
2
|
//
|
|
3
|
-
// The installer registers
|
|
4
|
-
//
|
|
3
|
+
// The installer registers TWO install-level cron lines inside one
|
|
4
|
+
// BEGIN/END-delimited per-brand block.
|
|
5
|
+
//
|
|
6
|
+
// 1. The scheduling-watcher heartbeat, once a minute, ticking
|
|
7
|
+
// `scheduling/mcp/dist/scripts/check-due-events.js`. The watcher
|
|
5
8
|
// scans every account itself (listLocalAccountIds → data/accounts/*/account.json)
|
|
6
9
|
// and only dispatches operator-created bookings whose time has come, so a
|
|
7
10
|
// per-minute tick creates no activity of its own.
|
|
8
11
|
//
|
|
12
|
+
// 2. The log rotator (Task 1706), every 15 minutes, ticking
|
|
13
|
+
// `platform/scripts/logs-rotate.sh`. cron is the mechanism because the platform
|
|
14
|
+
// ships zero systemd timers — this block is the established per-brand periodic
|
|
15
|
+
// surface, already merged idempotently here, already stripped by the
|
|
16
|
+
// uninstaller, already read back by admin `system-status`. darwin has no cron;
|
|
17
|
+
// its analogue is a StartInterval LaunchAgent authored in index.ts.
|
|
18
|
+
//
|
|
9
19
|
// Narrowed from the block removed in Task 039 (commit 08e64c1f7), which also
|
|
10
20
|
// reintroduces neither regression that removal fixed:
|
|
11
21
|
// - No `mkdir` of an account log dir. Logs go to <persistDir>/logs, outside
|
|
@@ -29,16 +39,28 @@ export function cronBlockMarkers(productName) {
|
|
|
29
39
|
const upper = productName.toUpperCase();
|
|
30
40
|
return { begin: `# BEGIN ${upper} CRONS`, end: `# END ${upper} CRONS` };
|
|
31
41
|
}
|
|
32
|
-
/** The
|
|
33
|
-
|
|
42
|
+
/** The four-line cron block: BEGIN marker, the heartbeat entry, the logs-rotate
|
|
43
|
+
* entry, END marker. Each entry carries a trailing `# <name>` comment — admin
|
|
44
|
+
* `system-status` names entries by exactly that (admin/mcp/src/index.ts:466),
|
|
45
|
+
* so the block self-reports as "2 registered: heartbeat, logs-rotate". */
|
|
46
|
+
export function buildBrandCronBlock(opts) {
|
|
34
47
|
const { begin, end } = cronBlockMarkers(opts.productName);
|
|
35
48
|
const script = `${opts.platformRoot}/plugins/scheduling/mcp/dist/scripts/check-due-events.js`;
|
|
36
49
|
const log = `${opts.logDir}/check-due-events.log`;
|
|
37
|
-
const
|
|
50
|
+
const heartbeat = `* * * * * PLATFORM_ROOT=${opts.platformRoot} ` +
|
|
38
51
|
`NEO4J_URI=bolt://localhost:${opts.neo4jPort} ` +
|
|
39
52
|
`MAXY_UI_INTERNAL_PORT=${opts.uiPort} ` +
|
|
40
53
|
`${opts.nodeBin} ${script} >> ${log} 2>&1 # heartbeat`;
|
|
41
|
-
|
|
54
|
+
// Task 1706. Every 15 minutes: at the ~1 MB/hour measured on sitedesk-code the
|
|
55
|
+
// live file stays within a megabyte of its 64 MiB bound. The rotator reads no
|
|
56
|
+
// env — its logs dir is a positional argument, so a co-resident brand can
|
|
57
|
+
// never rotate a sibling's logs. Its own lines go to server.log, the file it
|
|
58
|
+
// itself bounds; a dedicated rotator log would be one more unbounded file.
|
|
59
|
+
// cron's `>>` is open-append-close per tick, so it is unaffected by the
|
|
60
|
+
// rotator truncating that same file underneath it.
|
|
61
|
+
const rotateScript = `${opts.platformRoot}/scripts/logs-rotate.sh`;
|
|
62
|
+
const rotate = `*/15 * * * * ${rotateScript} ${opts.logDir} >> ${opts.logDir}/server.log 2>&1 # logs-rotate`;
|
|
63
|
+
return [begin, heartbeat, rotate, end].join("\n");
|
|
42
64
|
}
|
|
43
65
|
/** Escape a brand name for safe embedding in a RegExp (hyphens, dots). */
|
|
44
66
|
function escapeRegExp(s) {
|
|
@@ -67,7 +89,7 @@ export function stripCronBlock(crontab, productName) {
|
|
|
67
89
|
* fresh block. Re-running with the same block is idempotent — a re-install
|
|
68
90
|
* replaces the block, never stacks a second copy.
|
|
69
91
|
*/
|
|
70
|
-
export function
|
|
92
|
+
export function mergeBrandCronBlock(existing, block, productName) {
|
|
71
93
|
const { cleaned } = stripCronBlock(existing, productName);
|
|
72
94
|
return cleaned ? `${cleaned}\n${block}\n` : `${block}\n`;
|
|
73
95
|
}
|
package/dist/index.js
CHANGED
|
@@ -6,7 +6,7 @@ import { seedBypassPermissionsSettings, assertBypassPermissionsSeed } from "./pe
|
|
|
6
6
|
import { resolve, join, dirname } from "node:path";
|
|
7
7
|
import { randomBytes } from "node:crypto";
|
|
8
8
|
import { resolveInstallPortFromFs, buildMaxyUnitFile, buildClaudeSessionManagerUnitFile, buildClaudePtysSliceUnitFile, buildCloudflaredSliceUnitFile, buildDarwinServiceEnvPath } from "./port-resolution.js";
|
|
9
|
-
import {
|
|
9
|
+
import { buildBrandCronBlock, mergeBrandCronBlock } from "./cron-registration.js";
|
|
10
10
|
import { validateTierFlag, validateEntitlementBase64, applyTierToAccountConfig, entitlementPath } from "./tier-flag.js";
|
|
11
11
|
import { parseOsRelease, isUbuntuLike as isUbuntuLikePure, parseAptCacheCandidate, decideAptResolution, } from "./apt-resolve.js";
|
|
12
12
|
import { findPeerBrandOnDefaultNeo4jPort } from "./peer-brand-detect.js";
|
|
@@ -3503,10 +3503,11 @@ function resolveInstallAccountId() {
|
|
|
3503
3503
|
// ---------------------------------------------------------------------------
|
|
3504
3504
|
// Cron registration — scheduling-watcher heartbeat
|
|
3505
3505
|
//
|
|
3506
|
-
// Registers
|
|
3507
|
-
//
|
|
3508
|
-
// guards
|
|
3509
|
-
// — darwin runs under launchd and registers no
|
|
3506
|
+
// Registers the per-brand cron block: check-due-events.js every minute, and
|
|
3507
|
+
// (Task 1706) logs-rotate.sh every 15 minutes. See cron-registration.ts for the
|
|
3508
|
+
// line shapes and the regression guards. Idempotent across re-installs via the
|
|
3509
|
+
// BEGIN/END markers. Linux-only — darwin runs under launchd and registers no
|
|
3510
|
+
// cron, so it gets LaunchAgent analogues for both. The email dispatchers are
|
|
3510
3511
|
// deliberately NOT registered here.
|
|
3511
3512
|
//
|
|
3512
3513
|
// The log dir <persistDir>/logs is created earlier in installServiceLinux
|
|
@@ -3518,7 +3519,7 @@ function installCrons() {
|
|
|
3518
3519
|
const nodeBin = spawnSync("which", ["node"], { encoding: "utf-8" }).stdout.trim() || "/usr/bin/node";
|
|
3519
3520
|
const platformRoot = join(INSTALL_DIR, "platform");
|
|
3520
3521
|
const logDir = join(resolve(process.env.HOME ?? "/root", BRAND.configDir), "logs");
|
|
3521
|
-
const block =
|
|
3522
|
+
const block = buildBrandCronBlock({
|
|
3522
3523
|
productName: BRAND.productName,
|
|
3523
3524
|
platformRoot,
|
|
3524
3525
|
neo4jPort: NEO4J_PORT,
|
|
@@ -3531,14 +3532,14 @@ function installCrons() {
|
|
|
3531
3532
|
});
|
|
3532
3533
|
const existing = spawnSync("crontab", ["-l"], { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
|
|
3533
3534
|
const currentCrontab = existing.status === 0 ? existing.stdout : "";
|
|
3534
|
-
const newCrontab =
|
|
3535
|
+
const newCrontab = mergeBrandCronBlock(currentCrontab, block, BRAND.productName);
|
|
3535
3536
|
const write = spawnSync("crontab", ["-"], {
|
|
3536
3537
|
input: newCrontab,
|
|
3537
3538
|
encoding: "utf-8",
|
|
3538
3539
|
stdio: ["pipe", "pipe", "pipe"],
|
|
3539
3540
|
});
|
|
3540
3541
|
if (write.status === 0) {
|
|
3541
|
-
console.log(" Cron jobs: registered (heartbeat)");
|
|
3542
|
+
console.log(" Cron jobs: registered (heartbeat, logs-rotate)");
|
|
3542
3543
|
}
|
|
3543
3544
|
else {
|
|
3544
3545
|
console.error(` Cron jobs: failed to register — ${(write.stderr || "").trim()}`);
|
|
@@ -3846,7 +3847,7 @@ function installServiceDarwin() {
|
|
|
3846
3847
|
console.log(` Session manager not yet healthy on ${BRAND.claudeSessionManagerPort} — check ${join(logsDir, "server.log")}.`);
|
|
3847
3848
|
}
|
|
3848
3849
|
// Task 1403 — the scheduling heartbeat. On Linux a per-minute cron ticks
|
|
3849
|
-
// check-due-events.js (installCrons/
|
|
3850
|
+
// check-due-events.js (installCrons/buildBrandCronBlock); darwin has no
|
|
3850
3851
|
// cron and installCrons early-returns, so operator-created bookings would
|
|
3851
3852
|
// never dispatch. Author a StartInterval=60 LaunchAgent as the timer analogue:
|
|
3852
3853
|
// keepAlive=false (a periodic one-shot, not a supervised daemon). A bootstrap
|
|
@@ -3884,6 +3885,46 @@ function installServiceDarwin() {
|
|
|
3884
3885
|
console.error(` WARNING: heartbeat LaunchAgent ${hbLabel} bootstrap returned ${hbBoot.bootstrapStatus} after ${hbBoot.attempts} attempt(s): ${hbBoot.stderr} — scheduling ticks will not fire until a re-install succeeds.`);
|
|
3885
3886
|
logFile(` [create-maxy] launchd-plist=${hbLabel} loaded=false exit=${hbBoot.bootstrapStatus} attempts=${hbBoot.attempts}`);
|
|
3886
3887
|
}
|
|
3888
|
+
// Task 1706 — the log rotator. On Linux a */15 entry in the per-brand cron
|
|
3889
|
+
// block ticks logs-rotate.sh (installCrons/buildBrandCronBlock); darwin has no
|
|
3890
|
+
// cron and installCrons early-returns, so brand logs would grow unbounded.
|
|
3891
|
+
// Author a StartInterval=900 LaunchAgent as the timer analogue: keepAlive=false
|
|
3892
|
+
// (a periodic one-shot, not a supervised daemon). No wrapper script — unlike
|
|
3893
|
+
// the heartbeat the rotator reads no env; its logs dir is a positional arg,
|
|
3894
|
+
// which also means a co-resident brand can never rotate a sibling's logs. Its
|
|
3895
|
+
// own lines go to server.log, the file it itself bounds. A bootstrap failure
|
|
3896
|
+
// warns rather than aborting install — logs growing is a slow problem and the
|
|
3897
|
+
// next installer run retries.
|
|
3898
|
+
const lrLabel = `${launchdLabel()}-logs-rotate`;
|
|
3899
|
+
const lrPlistPath = join(launchAgentsDir(), `${lrLabel}.plist`);
|
|
3900
|
+
const lrScript = join(INSTALL_DIR, "platform", "scripts", "logs-rotate.sh");
|
|
3901
|
+
const lrPlist = renderPlist({
|
|
3902
|
+
label: lrLabel,
|
|
3903
|
+
programArguments: [lrScript, logsDir],
|
|
3904
|
+
stdoutPath: join(logsDir, "server.log"),
|
|
3905
|
+
stderrPath: join(logsDir, "server.log"),
|
|
3906
|
+
keepAlive: false,
|
|
3907
|
+
runAtLoad: false,
|
|
3908
|
+
startInterval: 900,
|
|
3909
|
+
});
|
|
3910
|
+
writeFileSync(lrPlistPath, lrPlist);
|
|
3911
|
+
logFile(` ${lrPlistPath} written (${lrPlist.length} bytes)`);
|
|
3912
|
+
const lrBoot = bootstrapLaunchAgent((args) => {
|
|
3913
|
+
const r = spawnSync("launchctl", args, { stdio: "pipe", encoding: "utf-8", timeout: 15_000 });
|
|
3914
|
+
return { status: r.status, stderr: (r.stderr ?? "").toString() };
|
|
3915
|
+
}, {
|
|
3916
|
+
gui: gui(),
|
|
3917
|
+
label: lrLabel,
|
|
3918
|
+
plistPath: lrPlistPath,
|
|
3919
|
+
logger: (line) => { console.log(` ${line}`); logFile(` ${line}`); },
|
|
3920
|
+
});
|
|
3921
|
+
if (lrBoot.ok) {
|
|
3922
|
+
logFile(` [create-maxy] launchd-plist=${lrLabel} loaded=true attempts=${lrBoot.attempts}`);
|
|
3923
|
+
}
|
|
3924
|
+
else {
|
|
3925
|
+
console.error(` WARNING: logs-rotate LaunchAgent ${lrLabel} bootstrap returned ${lrBoot.bootstrapStatus} after ${lrBoot.attempts} attempt(s): ${lrBoot.stderr} — brand logs will not be rotated until a re-install succeeds.`);
|
|
3926
|
+
logFile(` [create-maxy] launchd-plist=${lrLabel} loaded=false exit=${lrBoot.bootstrapStatus} attempts=${lrBoot.attempts}`);
|
|
3927
|
+
}
|
|
3887
3928
|
// Wait for the server to come up.
|
|
3888
3929
|
console.log(" Waiting for web server...");
|
|
3889
3930
|
let webServerUp = false;
|
package/dist/launchd-plist.js
CHANGED
|
@@ -55,7 +55,7 @@ export function buildSessionManagerWrapper(o) {
|
|
|
55
55
|
/**
|
|
56
56
|
* Bash wrapper for the darwin scheduling-heartbeat LaunchAgent (Task 1403).
|
|
57
57
|
* launchd's StartInterval timer is the analogue of the Linux per-minute cron
|
|
58
|
-
* (
|
|
58
|
+
* (buildBrandCronBlock) that ticks `check-due-events.js`. The wrapper
|
|
59
59
|
* sources the brand `.env` for NEO4J_URI + MAXY_UI_INTERNAL_PORT, exports
|
|
60
60
|
* PLATFORM_ROOT (the tick's own env contract, matching the cron line's
|
|
61
61
|
* `PLATFORM_ROOT=` prefix), and execs the tick once. launchd re-invokes it
|
package/dist/uninstall.js
CHANGED
|
@@ -79,14 +79,20 @@ function launchdLabel() {
|
|
|
79
79
|
return `com.rubytech.${BRAND.hostname}`;
|
|
80
80
|
}
|
|
81
81
|
/** Every LaunchAgent label installServiceDarwin() creates for this brand: the
|
|
82
|
-
* main server, the claude-session-manager (Task 1395),
|
|
83
|
-
* heartbeat (Task 1403). A non-default
|
|
84
|
-
* per-brand Neo4j LaunchAgent (Task 1396).
|
|
82
|
+
* main server, the claude-session-manager (Task 1395), the scheduling
|
|
83
|
+
* heartbeat (Task 1403), and the log rotator (Task 1706). A non-default
|
|
84
|
+
* (dedicated) brand also runs the per-brand Neo4j LaunchAgent (Task 1396).
|
|
85
|
+
* Uninstall boots out and removes all.
|
|
86
|
+
*
|
|
87
|
+
* This list is explicit, not a glob, so a LaunchAgent added to
|
|
88
|
+
* installServiceDarwin() and not added here survives uninstall and keeps
|
|
89
|
+
* ticking against a deleted install. Add both together, always. */
|
|
85
90
|
function darwinLaunchdLabels() {
|
|
86
91
|
const labels = [
|
|
87
92
|
launchdLabel(),
|
|
88
93
|
`${launchdLabel()}-claude-session-manager`,
|
|
89
94
|
`${launchdLabel()}-heartbeat`,
|
|
95
|
+
`${launchdLabel()}-logs-rotate`,
|
|
90
96
|
];
|
|
91
97
|
if ((BRAND.neo4jPort ?? SHARED_NEO4J_PORT) !== SHARED_NEO4J_PORT) {
|
|
92
98
|
labels.push(`${launchdLabel()}-neo4j`);
|
package/package.json
CHANGED
|
@@ -328,12 +328,15 @@ function objectPage(keys, opts = {}) {
|
|
|
328
328
|
success: true,
|
|
329
329
|
errors: [],
|
|
330
330
|
messages: [],
|
|
331
|
-
result: keys.map((
|
|
332
|
-
key,
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
331
|
+
result: keys.map((k) => {
|
|
332
|
+
const entry = typeof k === "string" ? { key: k, size: 10 } : k;
|
|
333
|
+
return {
|
|
334
|
+
key: entry.key,
|
|
335
|
+
size: entry.size,
|
|
336
|
+
etag: "e1",
|
|
337
|
+
last_modified: "2026-01-01T00:00:00Z",
|
|
338
|
+
};
|
|
339
|
+
}),
|
|
337
340
|
result_info: { cursor: opts.cursor ?? "", is_truncated: opts.truncated ?? false },
|
|
338
341
|
});
|
|
339
342
|
}
|
|
@@ -489,4 +492,111 @@ function objectPage(keys, opts = {}) {
|
|
|
489
492
|
strict_1.default.deepEqual((await cf.r2ObjectList("b")).map((o) => o.key), ["a.jpg", "b.jpg"]);
|
|
490
493
|
strict_1.default.equal(calls.length, 2);
|
|
491
494
|
});
|
|
495
|
+
// --- r2ObjectFind (Task 1697) ---------------------------------------------
|
|
496
|
+
//
|
|
497
|
+
// Sizing a get needs one record, not the prefix. These tests pin the two
|
|
498
|
+
// properties that make the early exit safe: it matches the exact key, and it
|
|
499
|
+
// stops at the page carrying it.
|
|
500
|
+
(0, node_test_1.default)("r2ObjectFind returns the exact key when a prefix neighbour is listed first", async () => {
|
|
501
|
+
// Not hypothetical. Measured 2026-07-16 against the live v4 API: prefix=
|
|
502
|
+
// a/photo.jpg really does return a/photo.jpg.bak BEFORE a/photo.jpg, because
|
|
503
|
+
// the listing is not in ascending UTF-8 binary order. Taking the first element
|
|
504
|
+
// would size the neighbour, which either rejects a legal get or admits an
|
|
505
|
+
// oversized one.
|
|
506
|
+
const { fetchFn } = recordingFetch({
|
|
507
|
+
ok: true,
|
|
508
|
+
status: 200,
|
|
509
|
+
body: objectPage([
|
|
510
|
+
{ key: "a/photo.jpg.bak", size: 999 },
|
|
511
|
+
{ key: "a/photo.jpg", size: 3 },
|
|
512
|
+
]),
|
|
513
|
+
});
|
|
514
|
+
const cf = (0, cf_exec_js_1.makeCfExec)(cred, throwingRun, fetchFn);
|
|
515
|
+
const found = await cf.r2ObjectFind("b", "a/photo.jpg");
|
|
516
|
+
strict_1.default.equal(found?.key, "a/photo.jpg");
|
|
517
|
+
strict_1.default.equal(found?.size, 3);
|
|
518
|
+
});
|
|
519
|
+
(0, node_test_1.default)("r2ObjectFind sends the key as the prefix, encoded", async () => {
|
|
520
|
+
const { fetchFn, calls } = recordingFetch({ ok: true, status: 200, body: objectPage([]) });
|
|
521
|
+
const cf = (0, cf_exec_js_1.makeCfExec)(cred, throwingRun, fetchFn);
|
|
522
|
+
await cf.r2ObjectFind("b", "person a/x.jpg");
|
|
523
|
+
strict_1.default.ok(calls[0].url.endsWith("/objects?prefix=person+a%2Fx.jpg"), calls[0].url);
|
|
524
|
+
});
|
|
525
|
+
(0, node_test_1.default)("r2ObjectFind stops at the page carrying the match and fetches no further page", async () => {
|
|
526
|
+
// The page says truncated with a cursor, so r2ObjectList would fetch page 2.
|
|
527
|
+
// sequenceFetch throws on an unexpected call, so a second fetch fails here.
|
|
528
|
+
const { fetchFn, calls } = sequenceFetch([
|
|
529
|
+
{
|
|
530
|
+
ok: true,
|
|
531
|
+
status: 200,
|
|
532
|
+
body: objectPage([{ key: "k", size: 7 }], { cursor: "c1", truncated: true }),
|
|
533
|
+
},
|
|
534
|
+
]);
|
|
535
|
+
const cf = (0, cf_exec_js_1.makeCfExec)(cred, throwingRun, fetchFn);
|
|
536
|
+
const found = await cf.r2ObjectFind("b", "k");
|
|
537
|
+
strict_1.default.equal(found?.size, 7);
|
|
538
|
+
strict_1.default.equal(calls.length, 1);
|
|
539
|
+
});
|
|
540
|
+
(0, node_test_1.default)("r2ObjectFind finds a match on a later page, never assuming position", async () => {
|
|
541
|
+
const { fetchFn, calls } = sequenceFetch([
|
|
542
|
+
{ ok: true, status: 200, body: objectPage(["x.jpg"], { cursor: "c1", truncated: true }) },
|
|
543
|
+
{ ok: true, status: 200, body: objectPage([{ key: "k", size: 5 }], { truncated: false }) },
|
|
544
|
+
]);
|
|
545
|
+
const cf = (0, cf_exec_js_1.makeCfExec)(cred, throwingRun, fetchFn);
|
|
546
|
+
const found = await cf.r2ObjectFind("b", "k");
|
|
547
|
+
strict_1.default.equal(found?.size, 5);
|
|
548
|
+
strict_1.default.equal(calls.length, 2);
|
|
549
|
+
});
|
|
550
|
+
(0, node_test_1.default)("r2ObjectFind returns null when the key is absent after exhausting pages", async () => {
|
|
551
|
+
const { fetchFn, calls } = sequenceFetch([
|
|
552
|
+
{ ok: true, status: 200, body: objectPage(["k.bak"], { cursor: "c1", truncated: true }) },
|
|
553
|
+
{ ok: true, status: 200, body: objectPage(["k.zzz"], { truncated: false }) },
|
|
554
|
+
]);
|
|
555
|
+
const cf = (0, cf_exec_js_1.makeCfExec)(cred, throwingRun, fetchFn);
|
|
556
|
+
strict_1.default.equal(await cf.r2ObjectFind("b", "k"), null);
|
|
557
|
+
strict_1.default.equal(calls.length, 2);
|
|
558
|
+
});
|
|
559
|
+
(0, node_test_1.default)("r2ObjectFind throws on a repeated cursor instead of looping forever", async () => {
|
|
560
|
+
const { fetchFn } = sequenceFetch([
|
|
561
|
+
{ ok: true, status: 200, body: objectPage(["x"], { cursor: "c1", truncated: true }) },
|
|
562
|
+
{ ok: true, status: 200, body: objectPage(["y"], { cursor: "c1", truncated: true }) },
|
|
563
|
+
]);
|
|
564
|
+
const cf = (0, cf_exec_js_1.makeCfExec)(cred, throwingRun, fetchFn);
|
|
565
|
+
await strict_1.default.rejects(() => cf.r2ObjectFind("b", "k"), /repeated pagination cursor/);
|
|
566
|
+
});
|
|
567
|
+
(0, node_test_1.default)("r2ObjectFind throws when truncated but no cursor is given", async () => {
|
|
568
|
+
const { fetchFn } = recordingFetch({
|
|
569
|
+
ok: true,
|
|
570
|
+
status: 200,
|
|
571
|
+
body: objectPage(["x"], { cursor: "", truncated: true }),
|
|
572
|
+
});
|
|
573
|
+
const cf = (0, cf_exec_js_1.makeCfExec)(cred, throwingRun, fetchFn);
|
|
574
|
+
await strict_1.default.rejects(() => cf.r2ObjectFind("b", "k"), /truncated but gave no pagination cursor/);
|
|
575
|
+
});
|
|
576
|
+
(0, node_test_1.default)("r2ObjectFind retains the body on a non-2xx", async () => {
|
|
577
|
+
const { fetchFn } = recordingFetch({ ok: false, status: 403, body: '{"errors":[{"code":10000}]}' });
|
|
578
|
+
const cf = (0, cf_exec_js_1.makeCfExec)(cred, throwingRun, fetchFn);
|
|
579
|
+
await strict_1.default.rejects(() => cf.r2ObjectFind("b", "k"), /10000/);
|
|
580
|
+
});
|
|
581
|
+
// Code review — the early exit changes observable behaviour on an anomalous
|
|
582
|
+
// page, and an unpinned behaviour change is how a regression starts. Pinned as a
|
|
583
|
+
// decision rather than left implicit.
|
|
584
|
+
//
|
|
585
|
+
// A page that reports truncated with no cursor is an anomaly r2ObjectList throws
|
|
586
|
+
// on, because a partial listing would corrupt the audit. r2ObjectFind returns at
|
|
587
|
+
// the match and never reaches that check, so the same response yields the size
|
|
588
|
+
// instead of an error. That is correct: the size is Cloudflare's own record for
|
|
589
|
+
// the exact key, the cap is enforced on it, and the anomaly concerns pages the
|
|
590
|
+
// sizing does not need. The audit's behaviour on the same response is unchanged
|
|
591
|
+
// and is pinned by "r2ObjectList throws when truncated but no cursor is given".
|
|
592
|
+
(0, node_test_1.default)("r2ObjectFind returns a match found before an anomalous page's guard is reached", async () => {
|
|
593
|
+
const { fetchFn } = recordingFetch({
|
|
594
|
+
ok: true,
|
|
595
|
+
status: 200,
|
|
596
|
+
body: objectPage([{ key: "k", size: 4 }], { cursor: "", truncated: true }),
|
|
597
|
+
});
|
|
598
|
+
const cf = (0, cf_exec_js_1.makeCfExec)(cred, throwingRun, fetchFn);
|
|
599
|
+
const found = await cf.r2ObjectFind("b", "k");
|
|
600
|
+
strict_1.default.equal(found?.size, 4);
|
|
601
|
+
});
|
|
492
602
|
//# sourceMappingURL=cf-exec.test.js.map
|