deepdebug-local-agent 1.0.23 → 1.1.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 (168) hide show
  1. package/.claude/context.md +76 -0
  2. package/.claude/skills/README.md +25 -0
  3. package/.claude/skills/tdd-debugging/SKILL.md +53 -0
  4. package/.claude/skills/tdd-debugging/references/anti-patterns.md +43 -0
  5. package/.claude/skills/tdd-debugging/references/command-reference.md +46 -0
  6. package/.claude/standards/README.md +51 -0
  7. package/.claude/standards/clean-code.md +75 -0
  8. package/.claude/standards/code-review-checklist.md +61 -0
  9. package/.claude/standards/layered-architecture.md +163 -0
  10. package/.claude/standards/node.md +45 -0
  11. package/.claude/standards/security-owasp.md +114 -0
  12. package/.claude/standards/simplicity.md +42 -0
  13. package/.claude/standards/solid.md +64 -0
  14. package/.claude/standards/testing.md +59 -0
  15. package/.cursor/rules/standards.mdc +17 -0
  16. package/.github/workflows/build-byoc-image.yml +315 -0
  17. package/.github/workflows/deploy-gcp.yml +102 -0
  18. package/.github/workflows/npm-publish.yml +69 -8
  19. package/AGENTS.md +21 -0
  20. package/CHANGELOG.md +69 -0
  21. package/CLAUDE.md +13 -0
  22. package/CLAUDE_CODE_AGENT_INFINITE.md +5 -3
  23. package/Dockerfile +4 -0
  24. package/Dockerfile.full +55 -3
  25. package/bin/install.js +16 -5
  26. package/cloudbuild-full.yaml +57 -0
  27. package/docker-compose.yml +5 -3
  28. package/docs/OBSERVABILITY.md +164 -0
  29. package/docs/byoc-agent-image.md +70 -0
  30. package/docs/graphify-packaging-plan.md +133 -0
  31. package/docs/graphify-validation-run-2026-10-28.md +243 -0
  32. package/docs/releasing.md +43 -0
  33. package/docs/signed-commands.md +163 -0
  34. package/env.example +30 -0
  35. package/helm/Chart.yaml +2 -2
  36. package/helm/templates/deployment.yaml +13 -3
  37. package/helm/templates/networkpolicy.yaml +22 -0
  38. package/helm/templates/secret.yaml +1 -0
  39. package/helm/values.yaml +13 -14
  40. package/mcp-server.js +4 -1
  41. package/package.json +9 -4
  42. package/scripts/upgrade-observability-push.sh +227 -0
  43. package/src/ai-engine.js +22 -22
  44. package/src/config/trusted-signing-keys.json +3 -0
  45. package/src/detectors/build-tool-detector.js +50 -0
  46. package/src/detectors/multi-project-detector.js +22 -22
  47. package/src/detectors/service-detector.js +24 -24
  48. package/src/exec-utils.js +26 -4
  49. package/src/git/base-git-provider.js +117 -117
  50. package/src/git/bitbucket-provider.js +2 -2
  51. package/src/git/git-provider-registry.js +18 -18
  52. package/src/git/github-provider.js +13 -13
  53. package/src/git/multi-repo-pr.js +161 -0
  54. package/src/mcp-http-server.js +32 -16
  55. package/src/observability/bundle-files.js +418 -0
  56. package/src/observability/detect.js +151 -0
  57. package/src/observability/provision.js +212 -0
  58. package/src/observability/render-cli.js +59 -0
  59. package/src/observability/routes.js +88 -0
  60. package/src/patch/patch-engine.js +28 -28
  61. package/src/patch-manager.js +85 -85
  62. package/src/patch_bkp.js +14 -14
  63. package/src/ports.js +4 -4
  64. package/src/routes/workspace.route.js +47 -16
  65. package/src/runtimes/base-runtime.js +75 -75
  66. package/src/runtimes/java/gradle-runtime.js +17 -17
  67. package/src/runtimes/java/java-integrations.js +25 -25
  68. package/src/runtimes/java/maven-runtime.js +34 -34
  69. package/src/runtimes/node/node-integrations.js +1 -1
  70. package/src/runtimes/node/npm-runtime.js +14 -14
  71. package/src/runtimes/node/yarn-runtime.js +4 -4
  72. package/src/runtimes/runtime-registry.js +25 -25
  73. package/src/server-local.js +4 -4
  74. package/src/server.js +1366 -333
  75. package/src/siblings/burst-suppressor.js +89 -0
  76. package/src/siblings/capabilities.js +273 -0
  77. package/src/siblings/env-compat.cjs +58 -0
  78. package/src/siblings/incident-forwarder.js +320 -0
  79. package/src/siblings/logger.js +15 -0
  80. package/src/siblings/metrics.js +94 -0
  81. package/src/utils/agent-auth.js +184 -0
  82. package/src/utils/approval-token.js +56 -0
  83. package/src/utils/bind-host.js +29 -0
  84. package/src/utils/branch-refspec.js +98 -0
  85. package/src/utils/child-env.js +34 -0
  86. package/src/utils/environment-diagnostics.js +55 -55
  87. package/src/utils/exec-policy.js +962 -0
  88. package/src/utils/exec-utils.js +15 -15
  89. package/src/utils/file-count.js +46 -0
  90. package/src/utils/fs-utils.js +10 -10
  91. package/src/utils/multi-repo-open.js +132 -0
  92. package/src/utils/open-gate.js +183 -0
  93. package/src/utils/ranged-read.js +65 -0
  94. package/src/utils/request-guards.js +77 -0
  95. package/src/utils/request-signing.js +342 -0
  96. package/src/utils/root-recovery.js +244 -0
  97. package/src/utils/safe-exec-args.js +58 -0
  98. package/src/utils/sandbox.js +182 -0
  99. package/src/utils/search-outcome.js +56 -0
  100. package/src/utils/workspace-root.js +80 -0
  101. package/src/validators/syntax-validator.js +149 -0
  102. package/src/workspace/detect-port.js +16 -16
  103. package/src/workspace/file-reader.js +1 -1
  104. package/src/workspace/process-manager.js +46 -46
  105. package/src/workspace-manager.js +86 -24
  106. package/test/adapters/registry.test.js +9 -2
  107. package/test/agent-auth.test.js +194 -0
  108. package/test/bind-host.test.js +26 -0
  109. package/test/branch-refspec.test.js +224 -0
  110. package/test/burst-suppressor.test.js +60 -0
  111. package/test/capabilities-access.test.js +47 -0
  112. package/test/capabilities.test.js +87 -0
  113. package/test/child-env.test.js +23 -0
  114. package/test/env-compat.test.js +71 -0
  115. package/test/exec-policy.test.js +463 -0
  116. package/test/file-count.test.js +64 -0
  117. package/test/incident-forwarder.test.js +231 -0
  118. package/test/multi-repo-open.test.js +180 -0
  119. package/test/multi-repo-pr.test.js +129 -0
  120. package/test/observability-detect.test.js +199 -0
  121. package/test/open-gate.test.js +341 -0
  122. package/test/ranged-read.test.js +60 -0
  123. package/test/request-guards.test.js +91 -0
  124. package/test/request-signing.test.js +284 -0
  125. package/test/root-recovery-sidecar.test.js +171 -0
  126. package/test/root-recovery.test.js +274 -0
  127. package/test/safe-exec-args.test.js +58 -0
  128. package/test/sandbox.test.js +98 -0
  129. package/test/search-outcome.test.js +50 -0
  130. package/test/workspace-root.test.js +64 -0
  131. package/tools/graphify-validation/README.md +92 -0
  132. package/tools/graphify-validation/config/questions.json +31 -0
  133. package/tools/graphify-validation/config/repos.json +9 -0
  134. package/tools/graphify-validation/config/symbols.json +6 -0
  135. package/tools/graphify-validation/fixtures/spring/expected.json +34 -0
  136. package/tools/graphify-validation/fixtures/spring/src/main/java/com/example/fx/AppConfig.java +29 -0
  137. package/tools/graphify-validation/fixtures/spring/src/main/java/com/example/fx/AuditLogger.java +15 -0
  138. package/tools/graphify-validation/fixtures/spring/src/main/java/com/example/fx/Cache.java +7 -0
  139. package/tools/graphify-validation/fixtures/spring/src/main/java/com/example/fx/CheckoutService.java +25 -0
  140. package/tools/graphify-validation/fixtures/spring/src/main/java/com/example/fx/InMemoryCache.java +18 -0
  141. package/tools/graphify-validation/fixtures/spring/src/main/java/com/example/fx/OrderController.java +22 -0
  142. package/tools/graphify-validation/fixtures/spring/src/main/java/com/example/fx/PaymentGateway.java +5 -0
  143. package/tools/graphify-validation/fixtures/spring/src/main/java/com/example/fx/PaypalGateway.java +11 -0
  144. package/tools/graphify-validation/fixtures/spring/src/main/java/com/example/fx/RefundService.java +14 -0
  145. package/tools/graphify-validation/fixtures/spring/src/main/java/com/example/fx/StripeGateway.java +11 -0
  146. package/tools/graphify-validation/lib/affected_vs_rg.py +81 -0
  147. package/tools/graphify-validation/lib/audit_hook/sitecustomize.py +22 -0
  148. package/tools/graphify-validation/lib/graph_metrics.py +96 -0
  149. package/tools/graphify-validation/lib/gv_common.py +130 -0
  150. package/tools/graphify-validation/lib/licenses.py +47 -0
  151. package/tools/graphify-validation/lib/pip_audit_summary.py +18 -0
  152. package/tools/graphify-validation/lib/recall.py +115 -0
  153. package/tools/graphify-validation/lib/report.py +226 -0
  154. package/tools/graphify-validation/lib/run_all.py +237 -0
  155. package/tools/graphify-validation/lib/run_graphify.py +144 -0
  156. package/tools/graphify-validation/lib/rusage_helper.py +17 -0
  157. package/tools/graphify-validation/lib/sample_edges.py +246 -0
  158. package/tools/graphify-validation/lib/spring_fixture.py +81 -0
  159. package/tools/graphify-validation/requirements-py311.lock +508 -0
  160. package/tools/graphify-validation/requirements-tools.in +1 -0
  161. package/tools/graphify-validation/requirements-tools.lock +371 -0
  162. package/tools/graphify-validation/requirements.in +1 -0
  163. package/tools/graphify-validation/requirements.lock +502 -0
  164. package/tools/graphify-validation/run.sh +43 -0
  165. package/tools/graphify-validation/tests/test_harness.py +151 -0
  166. package/cloudbuild-agent-qa.yaml +0 -43
  167. package/cloudbuild-agent-staging.yaml +0 -40
  168. package/cloudbuild.yaml.deprecated +0 -42
@@ -0,0 +1,194 @@
1
+ // test/agent-auth.test.js
2
+ // SECURITY AUDIT 2026-09: the agent's HTTP API (shell exec, file write) was
3
+ // unauthenticated on 0.0.0.0 with CORS allowing any origin outside
4
+ // NODE_ENV=production. These tests pin the auth perimeter.
5
+
6
+ import { test, describe } from 'node:test';
7
+ import assert from 'node:assert/strict';
8
+ import http from 'node:http';
9
+ import express from 'express';
10
+ import {
11
+ resolveAuthConfig,
12
+ authorize,
13
+ createAuthMiddleware,
14
+ internalAuthHeaders,
15
+ isLoopbackAddress,
16
+ isLoopbackHostHeader,
17
+ allowedOrigins,
18
+ corsOriginCallback,
19
+ describeAuthMode,
20
+ } from '../src/utils/agent-auth.js';
21
+
22
+ const ORIGINS = allowedOrigins({});
23
+ const req = (over = {}) => ({
24
+ method: 'POST',
25
+ path: '/workspace/exec',
26
+ headers: { host: 'localhost:5055' },
27
+ remoteAddress: '127.0.0.1',
28
+ ...over,
29
+ });
30
+
31
+ describe('resolveAuthConfig', () => {
32
+ test('LOCAL_AGENT_TOKEN selects enforced token mode', () => {
33
+ assert.deepEqual(resolveAuthConfig({ LOCAL_AGENT_TOKEN: 't' }), { mode: 'token', token: 't' });
34
+ });
35
+ test('AGENT_SHARED_SECRET alone (BYOC ACI) IS enforced: the gateway sends it as X-Agent-Token', () => {
36
+ const cfg = resolveAuthConfig({ AGENT_SHARED_SECRET: 's3cret' });
37
+ assert.equal(cfg.mode, 'token');
38
+ assert.equal(cfg.token, 's3cret');
39
+ assert.equal(cfg.source, 'AGENT_SHARED_SECRET');
40
+ });
41
+ test('LOCAL_AGENT_AUTH=compat is the explicit opt-out that keeps a shared secret unenforced', () => {
42
+ const cfg = resolveAuthConfig({ AGENT_SHARED_SECRET: 's3cret', LOCAL_AGENT_AUTH: 'compat' });
43
+ assert.equal(cfg.mode, 'compat');
44
+ assert.equal(cfg.token, 's3cret');
45
+ assert.equal(cfg.explicit, true);
46
+ });
47
+ test('LOCAL_AGENT_TOKEN wins over AGENT_SHARED_SECRET', () => {
48
+ assert.deepEqual(resolveAuthConfig({ AGENT_SHARED_SECRET: 's', LOCAL_AGENT_TOKEN: 't' }), { mode: 'token', token: 't' });
49
+ });
50
+ test('Cloud Run without token is compat (IAM perimeter)', () => {
51
+ const cfg = resolveAuthConfig({ K_SERVICE: 'deepdebug-local-agent-qa' });
52
+ assert.equal(cfg.mode, 'compat');
53
+ assert.equal(cfg.cloudRun, true);
54
+ });
55
+ test('default outside Cloud Run with no token is loopback-only (remote callers refused)', () => {
56
+ const cfg = resolveAuthConfig({});
57
+ assert.equal(cfg.mode, 'loopback');
58
+ assert.equal(cfg.implicit, true);
59
+ });
60
+ test('loopback-only is opt-in', () => {
61
+ assert.equal(resolveAuthConfig({ LOCAL_AGENT_AUTH: 'loopback' }).mode, 'loopback');
62
+ });
63
+ test('explicit opt-out', () => {
64
+ assert.equal(resolveAuthConfig({ LOCAL_AGENT_AUTH: 'off', LOCAL_AGENT_TOKEN: 's' }).mode, 'off');
65
+ });
66
+ test('describeAuthMode warns that the token is not enforced only in compat', () => {
67
+ assert.match(describeAuthMode(resolveAuthConfig({ AGENT_SHARED_SECRET: 's', LOCAL_AGENT_AUTH: 'compat' })), /token not enforced/);
68
+ assert.match(describeAuthMode(resolveAuthConfig({ AGENT_SHARED_SECRET: 's' })), /enforcing AGENT_SHARED_SECRET/);
69
+ assert.match(describeAuthMode(resolveAuthConfig({})), /remote callers refused/);
70
+ });
71
+ });
72
+
73
+ describe('authorize — AGENT_SHARED_SECRET (BYOC ACI) is enforced', () => {
74
+ const cfg = resolveAuthConfig({ AGENT_SHARED_SECRET: 's3cret' });
75
+ test('ACI gateway call WITHOUT X-Agent-Token is refused (was: passed in compat)', () => {
76
+ const d = authorize(req({ remoteAddress: '20.1.2.3', headers: { host: 'deepdebug-agent-ab.eastus.azurecontainer.io' } }), cfg, ORIGINS);
77
+ assert.equal(d.ok, false);
78
+ assert.equal(d.status, 401);
79
+ });
80
+ test('ACI gateway call WITH the shared secret passes', () => {
81
+ const d = authorize(req({ remoteAddress: '20.1.2.3', headers: { host: 'deepdebug-agent-ab.eastus.azurecontainer.io', 'x-agent-token': 's3cret' } }), cfg, ORIGINS);
82
+ assert.equal(d.ok, true);
83
+ });
84
+ test('explicit LOCAL_AGENT_AUTH=compat keeps the old pass-through', () => {
85
+ const compat = resolveAuthConfig({ AGENT_SHARED_SECRET: 's3cret', LOCAL_AGENT_AUTH: 'compat' });
86
+ assert.equal(authorize(req({ remoteAddress: '20.1.2.3', headers: { host: 'x.azurecontainer.io' } }), compat, ORIGINS).ok, true);
87
+ });
88
+ test('foreign browser Origin is refused', () => {
89
+ assert.equal(authorize(req({ headers: { host: 'localhost:5055', origin: 'https://attacker.vercel.app' } }), cfg, ORIGINS).ok, false);
90
+ });
91
+ test('internal tunnel calls forward the shared secret', () => {
92
+ assert.deepEqual(internalAuthHeaders(cfg), { 'X-Agent-Token': 's3cret' });
93
+ });
94
+ });
95
+
96
+ describe('authorize — loopback mode', () => {
97
+ const cfg = { mode: 'loopback', token: null };
98
+ test('in-process tunnel call (loopback, localhost Host, no Origin) is allowed', () => {
99
+ assert.equal(authorize(req(), cfg, ORIGINS).ok, true);
100
+ });
101
+ test('IPv4-mapped loopback is allowed', () => {
102
+ assert.equal(authorize(req({ remoteAddress: '::ffff:127.0.0.1' }), cfg, ORIGINS).ok, true);
103
+ });
104
+ test('LAN caller is rejected', () => {
105
+ const d = authorize(req({ remoteAddress: '192.168.1.20' }), cfg, ORIGINS);
106
+ assert.equal(d.ok, false);
107
+ assert.equal(d.status, 401);
108
+ });
109
+ test('docker bridge caller is rejected', () => {
110
+ assert.equal(authorize(req({ remoteAddress: '172.17.0.1' }), cfg, ORIGINS).ok, false);
111
+ });
112
+ test('DNS rebinding (loopback socket, foreign Host) is rejected', () => {
113
+ const d = authorize(req({ headers: { host: 'evil.example:5055' } }), cfg, ORIGINS);
114
+ assert.equal(d.ok, false);
115
+ assert.equal(d.status, 403);
116
+ });
117
+ test('drive-by from a web page (foreign Origin) is rejected', () => {
118
+ const d = authorize(req({ headers: { host: 'localhost:5055', origin: 'https://attacker.vercel.app' } }), cfg, ORIGINS);
119
+ assert.equal(d.ok, false);
120
+ });
121
+ test('/health stays public for probes', () => {
122
+ assert.equal(authorize(req({ method: 'GET', path: '/health', remoteAddress: '10.0.0.9' }), cfg, ORIGINS).ok, true);
123
+ });
124
+ test('/metrics stays public for scrapes', () => {
125
+ assert.equal(authorize(req({ method: 'GET', path: '/metrics', remoteAddress: '10.0.0.9' }), cfg, ORIGINS).ok, true);
126
+ });
127
+ test('POST /health is not public', () => {
128
+ assert.equal(authorize(req({ method: 'POST', path: '/health', remoteAddress: '10.0.0.9' }), cfg, ORIGINS).ok, false);
129
+ });
130
+ });
131
+
132
+ describe('authorize — token mode', () => {
133
+ const cfg = { mode: 'token', token: 'correct-horse' };
134
+ test('valid X-Agent-Token from a remote caller is allowed', () => {
135
+ const d = authorize(req({ remoteAddress: '20.1.2.3', headers: { host: 'agent.azurecontainer.io', 'x-agent-token': 'correct-horse' } }), cfg, ORIGINS);
136
+ assert.equal(d.ok, true);
137
+ });
138
+ test('missing token is rejected even from loopback', () => {
139
+ assert.equal(authorize(req(), cfg, ORIGINS).ok, false);
140
+ });
141
+ test('wrong / prefix token is rejected', () => {
142
+ assert.equal(authorize(req({ headers: { host: 'x', 'x-agent-token': 'correct' } }), cfg, ORIGINS).ok, false);
143
+ assert.equal(authorize(req({ headers: { host: 'x', 'x-agent-token': 'correct-horse2' } }), cfg, ORIGINS).ok, false);
144
+ });
145
+ test('Authorization bearer is NOT accepted (on Cloud Run it carries the Google ID token)', () => {
146
+ assert.equal(authorize(req({ headers: { host: 'x', authorization: 'Bearer correct-horse' } }), cfg, ORIGINS).ok, false);
147
+ });
148
+ test('internalAuthHeaders carries the token for the WS tunnel', () => {
149
+ assert.deepEqual(internalAuthHeaders(cfg), { 'X-Agent-Token': 'correct-horse' });
150
+ assert.deepEqual(internalAuthHeaders({ mode: 'loopback', token: null }), {});
151
+ });
152
+ });
153
+
154
+ describe('helpers', () => {
155
+ test('isLoopbackAddress', () => {
156
+ for (const a of ['127.0.0.1', '127.9.9.9', '::1', '::ffff:127.0.0.1']) assert.equal(isLoopbackAddress(a), true, a);
157
+ for (const a of ['0.0.0.0', '10.0.0.1', '::ffff:10.0.0.1', '', undefined]) assert.equal(isLoopbackAddress(a), false, String(a));
158
+ });
159
+ test('isLoopbackHostHeader', () => {
160
+ for (const h of ['localhost', 'localhost:5055', '127.0.0.1:5055', '[::1]:5055']) assert.equal(isLoopbackHostHeader(h), true, h);
161
+ for (const h of ['localhost.evil.com', 'evil.com', '127.0.0.1.nip.io:5055', undefined]) assert.equal(isLoopbackHostHeader(h), false, String(h));
162
+ });
163
+ test('CORS allowlist has no wildcard hosting domains', () => {
164
+ const cb = corsOriginCallback(ORIGINS);
165
+ const verdict = (o) => { let r; cb(o, (_e, ok) => { r = ok; }); return r; };
166
+ assert.equal(verdict('http://localhost:3010'), true);
167
+ assert.equal(verdict(undefined), true);
168
+ assert.equal(verdict('https://anything.vercel.app'), false);
169
+ assert.equal(verdict('https://anything.run.app'), false);
170
+ assert.equal(verdict('https://x.lovable.app'), false);
171
+ });
172
+ });
173
+
174
+ describe('createAuthMiddleware (express, real socket)', () => {
175
+ test('rejects a token-less exec call, accepts with token, /health public', async () => {
176
+ const app = express();
177
+ app.use(createAuthMiddleware({ mode: 'token', token: 'tkn' }, ORIGINS));
178
+ app.get('/health', (_q, r) => r.json({ ok: true }));
179
+ app.post('/workspace/exec', (_q, r) => r.json({ ran: true }));
180
+ const server = http.createServer(app);
181
+ await new Promise((res) => server.listen(0, '127.0.0.1', res));
182
+ const base = `http://127.0.0.1:${server.address().port}`;
183
+ try {
184
+ assert.equal((await fetch(`${base}/health`)).status, 200);
185
+ const denied = await fetch(`${base}/workspace/exec`, { method: 'POST' });
186
+ assert.equal(denied.status, 401);
187
+ const ok = await fetch(`${base}/workspace/exec`, { method: 'POST', headers: { 'X-Agent-Token': 'tkn' } });
188
+ assert.equal(ok.status, 200);
189
+ assert.deepEqual(await ok.json(), { ran: true });
190
+ } finally {
191
+ server.close();
192
+ }
193
+ });
194
+ });
@@ -0,0 +1,26 @@
1
+ // test/bind-host.test.js — LOCAL_AGENT_BIND_HOST (ACI TLS sidecar binds the agent to loopback).
2
+ import { test } from 'node:test';
3
+ import assert from 'node:assert/strict';
4
+ import { resolveBindHost, DEFAULT_BIND_HOST } from '../src/utils/bind-host.js';
5
+
6
+ test('defaults to loopback unless the auth mode is remote-capable (fail-closed)', () => {
7
+ assert.equal(resolveBindHost({}), '127.0.0.1');
8
+ assert.equal(resolveBindHost({ LOCAL_AGENT_BIND_HOST: ' ' }, { mode: 'loopback', implicit: true }), '127.0.0.1');
9
+ assert.equal(resolveBindHost({}, { mode: 'token', token: 't' }), DEFAULT_BIND_HOST);
10
+ assert.equal(resolveBindHost({}, { mode: 'compat', cloudRun: true }), '0.0.0.0');
11
+ assert.equal(resolveBindHost({}, { mode: 'off' }), '0.0.0.0');
12
+ });
13
+
14
+ test('explicit LOCAL_AGENT_BIND_HOST wins over the auth-derived default', () => {
15
+ assert.equal(resolveBindHost({ LOCAL_AGENT_BIND_HOST: '0.0.0.0' }, { mode: 'loopback' }), '0.0.0.0');
16
+ });
17
+
18
+ test('accepts loopback / explicit IPs', () => {
19
+ assert.equal(resolveBindHost({ LOCAL_AGENT_BIND_HOST: '127.0.0.1' }), '127.0.0.1');
20
+ assert.equal(resolveBindHost({ LOCAL_AGENT_BIND_HOST: 'localhost' }), '127.0.0.1');
21
+ assert.equal(resolveBindHost({ LOCAL_AGENT_BIND_HOST: '::1' }), '::1');
22
+ });
23
+
24
+ test('invalid values fall back to the default instead of crashing', () => {
25
+ assert.equal(resolveBindHost({ LOCAL_AGENT_BIND_HOST: 'not a host;rm -rf' }), '127.0.0.1');
26
+ });
@@ -0,0 +1,224 @@
1
+ // test/branch-refspec.test.js
2
+ // REFSPEC FIX: the /workspace/open REUSE path never saw branches created
3
+ // AFTER the original `git clone --single-branch` (the remote refspec stays
4
+ // restricted to the cloned branch, `git fetch origin` refreshes only that,
5
+ // the checkout error is swallowed and `git reset --hard origin/<branch>`
6
+ // dies with "ambiguous argument ... unknown revision" -> 500). Proven in QA
7
+ // 2026-07-20 (agent rev 00021-kd8): auto-fix cloned vh-e2e-legacy-java8
8
+ // single-branch on main, minutes later the code-review asked for the PR
9
+ // branch fix/autonomousai_order-service-index-out-of-bounds -> 500 ->
10
+ // review FAILED (PR #13 row in ai_pr_reviews). The fix fetches the
11
+ // REQUESTED ref explicitly before checkout/reset.
12
+
13
+ import { test, describe, before, after } from 'node:test';
14
+ import assert from 'node:assert/strict';
15
+ import fs from 'node:fs';
16
+ import path from 'node:path';
17
+ import os from 'node:os';
18
+ import { exec } from 'node:child_process';
19
+ import { promisify } from 'node:util';
20
+ import {
21
+ isSafeBranchName,
22
+ explicitBranchRefspec,
23
+ isRemoteBranchMissing,
24
+ fetchRequestedRef,
25
+ } from '../src/utils/branch-refspec.js';
26
+
27
+ const execAsync = promisify(exec);
28
+
29
+ const QA_PR_BRANCH = 'fix/autonomousai_order-service-index-out-of-bounds';
30
+
31
+ describe('isSafeBranchName', () => {
32
+ test('accepts real-world branch names (incl. the QA PR branch)', () => {
33
+ for (const b of [
34
+ 'main',
35
+ 'master',
36
+ 'develop',
37
+ QA_PR_BRANCH,
38
+ 'feature/multi-repo-wiring',
39
+ 'release-1.2.3',
40
+ 'hotfix/JIRA-123_null-check',
41
+ 'user/some.dotted.name',
42
+ ]) {
43
+ assert.equal(isSafeBranchName(b), true, `should accept: ${b}`);
44
+ }
45
+ });
46
+
47
+ test('rejects shell metacharacters (branch is interpolated into a shell line)', () => {
48
+ for (const b of [
49
+ 'main; rm -rf /',
50
+ 'main && curl evil',
51
+ 'main`id`',
52
+ 'main$(id)',
53
+ 'main|tee /etc/passwd',
54
+ 'main > /tmp/x',
55
+ "main'",
56
+ 'main"',
57
+ 'ma in',
58
+ 'main\nother',
59
+ 'main\\other',
60
+ ]) {
61
+ assert.equal(isSafeBranchName(b), false, `should reject: ${b}`);
62
+ }
63
+ });
64
+
65
+ test('rejects git-ref footguns and degenerate inputs', () => {
66
+ for (const b of [
67
+ '-oProxyCommand=evil', // leading dash = option injection
68
+ '.hidden', // leading dot
69
+ 'a..b', // revision range
70
+ 'a//b',
71
+ 'a/',
72
+ 'a.lock',
73
+ '',
74
+ ' ',
75
+ null,
76
+ undefined,
77
+ 42,
78
+ 'x'.repeat(256),
79
+ ]) {
80
+ assert.equal(isSafeBranchName(b), false, `should reject: ${String(b)}`);
81
+ }
82
+ });
83
+ });
84
+
85
+ describe('explicitBranchRefspec', () => {
86
+ test('builds the +refs/heads/<b>:refs/remotes/origin/<b> refspec', () => {
87
+ assert.equal(
88
+ explicitBranchRefspec(QA_PR_BRANCH),
89
+ `+refs/heads/${QA_PR_BRANCH}:refs/remotes/origin/${QA_PR_BRANCH}`
90
+ );
91
+ assert.equal(explicitBranchRefspec('main'), '+refs/heads/main:refs/remotes/origin/main');
92
+ });
93
+
94
+ test('throws on unsafe names (defense in depth, never emits injectable text)', () => {
95
+ assert.throws(() => explicitBranchRefspec('main; rm -rf /'));
96
+ assert.throws(() => explicitBranchRefspec(''));
97
+ });
98
+ });
99
+
100
+ describe('isRemoteBranchMissing', () => {
101
+ test('matches the git messages for a nonexistent remote branch', () => {
102
+ assert.equal(isRemoteBranchMissing(
103
+ "Command failed: git fetch origin\nfatal: couldn't find remote ref refs/heads/nope"
104
+ ), true);
105
+ assert.equal(isRemoteBranchMissing(
106
+ 'fatal: Remote branch nope not found in upstream origin'
107
+ ), true);
108
+ });
109
+
110
+ test('does NOT match auth/network/other failures (those keep 500 semantics)', () => {
111
+ assert.equal(isRemoteBranchMissing('Authentication failed for https://...'), false);
112
+ assert.equal(isRemoteBranchMissing('fatal: unable to access ... Could not resolve host'), false);
113
+ assert.equal(isRemoteBranchMissing(''), false);
114
+ assert.equal(isRemoteBranchMissing(null), false);
115
+ assert.equal(isRemoteBranchMissing(undefined), false);
116
+ });
117
+ });
118
+
119
+ describe('fetchRequestedRef (injected exec - pins the exact command)', () => {
120
+ test('QA failure mode: branch outside the single-branch refspec -> explicit fetch with the right refspec', async () => {
121
+ const calls = [];
122
+ const fakeExec = async (cmd, opts) => { calls.push({ cmd, opts }); };
123
+ await fetchRequestedRef(fakeExec, QA_PR_BRANCH, '/tmp/workspaces/VISIONHAMMER_vh-e2e-legacy-java8');
124
+ assert.equal(calls.length, 1);
125
+ assert.equal(
126
+ calls[0].cmd,
127
+ `git fetch origin "+refs/heads/${QA_PR_BRANCH}:refs/remotes/origin/${QA_PR_BRANCH}"`
128
+ );
129
+ assert.equal(calls[0].opts.cwd, '/tmp/workspaces/VISIONHAMMER_vh-e2e-legacy-java8');
130
+ });
131
+
132
+ test('branch nonexistent on remote -> tagged BRANCH_NOT_FOUND with a clear message (not a generic 500)', async () => {
133
+ const fakeExec = async () => {
134
+ throw new Error("Command failed: git fetch\nfatal: couldn't find remote ref refs/heads/nope");
135
+ };
136
+ await assert.rejects(
137
+ () => fetchRequestedRef(fakeExec, 'nope', '/tmp/x'),
138
+ (err) => {
139
+ assert.equal(err.code, 'BRANCH_NOT_FOUND');
140
+ assert.equal(err.httpStatus, 404);
141
+ assert.equal(err.message, 'branch not found on remote: nope');
142
+ return true;
143
+ }
144
+ );
145
+ });
146
+
147
+ test('shell-unsafe branch -> tagged INVALID_BRANCH, exec is NEVER called', async () => {
148
+ let called = 0;
149
+ const fakeExec = async () => { called++; };
150
+ await assert.rejects(
151
+ () => fetchRequestedRef(fakeExec, 'main; rm -rf /', '/tmp/x'),
152
+ (err) => {
153
+ assert.equal(err.code, 'INVALID_BRANCH');
154
+ assert.equal(err.httpStatus, 400);
155
+ return true;
156
+ }
157
+ );
158
+ assert.equal(called, 0);
159
+ });
160
+
161
+ test('other fetch failures (auth/network) are rethrown UNTOUCHED (existing catch semantics)', async () => {
162
+ const boom = new Error('fatal: Authentication failed');
163
+ const fakeExec = async () => { throw boom; };
164
+ await assert.rejects(() => fetchRequestedRef(fakeExec, 'main', '/tmp/x'), (err) => err === boom);
165
+ });
166
+ });
167
+
168
+ describe('fetchRequestedRef (real git - reproduces the QA scenario end-to-end)', () => {
169
+ let base, originDir, cloneDir;
170
+ const GIT_ID = '-c user.email="t@t" -c user.name="t"';
171
+
172
+ before(async () => {
173
+ base = fs.mkdtempSync(path.join(os.tmpdir(), 'branch-refspec-'));
174
+ originDir = path.join(base, 'origin-repo');
175
+ cloneDir = path.join(base, 'clone');
176
+ fs.mkdirSync(originDir, { recursive: true });
177
+ // origin repo with main only
178
+ await execAsync(`git init -b main`, { cwd: originDir });
179
+ fs.writeFileSync(path.join(originDir, 'a.txt'), 'v1\n');
180
+ await execAsync(`git add . && git ${GIT_ID} commit -m init`, { cwd: originDir });
181
+ // the auto-fix session clone: single-branch on main (restricts the refspec)
182
+ await execAsync(`git clone --branch main --single-branch "file://${originDir}" "${cloneDir}"`);
183
+ // the PR branch is born on the remote AFTER the clone
184
+ await execAsync(`git checkout -b ${QA_PR_BRANCH}`, { cwd: originDir });
185
+ fs.writeFileSync(path.join(originDir, 'a.txt'), 'v2-fix\n');
186
+ await execAsync(`git ${GIT_ID} commit -am fix`, { cwd: originDir });
187
+ await execAsync(`git checkout main`, { cwd: originDir });
188
+ });
189
+
190
+ after(() => {
191
+ try { fs.rmSync(base, { recursive: true, force: true }); } catch (_) {}
192
+ });
193
+
194
+ test('plain `git fetch origin` does NOT bring the new branch (the bug)', async () => {
195
+ await execAsync(`git fetch origin`, { cwd: cloneDir });
196
+ await assert.rejects(() => execAsync(`git rev-parse --verify origin/${QA_PR_BRANCH}`, { cwd: cloneDir }));
197
+ });
198
+
199
+ test('explicit ref fetch makes checkout + reset --hard succeed (the fix)', async () => {
200
+ await fetchRequestedRef((cmd, opts) => execAsync(cmd, opts), QA_PR_BRANCH, cloneDir);
201
+ await execAsync(`git rev-parse --verify origin/${QA_PR_BRANCH}`, { cwd: cloneDir });
202
+ // the exact reuse sequence from server.js now succeeds
203
+ await execAsync(`git checkout ${QA_PR_BRANCH} 2>/dev/null || git checkout -b ${QA_PR_BRANCH} origin/${QA_PR_BRANCH}`, { cwd: cloneDir });
204
+ await execAsync(`git reset --hard origin/${QA_PR_BRANCH}`, { cwd: cloneDir });
205
+ assert.equal(fs.readFileSync(path.join(cloneDir, 'a.txt'), 'utf8'), 'v2-fix\n');
206
+ });
207
+
208
+ test('branch already in the refspec (main) -> explicit fetch is a no-op, behavior unchanged', async () => {
209
+ await fetchRequestedRef((cmd, opts) => execAsync(cmd, opts), 'main', cloneDir);
210
+ await execAsync(`git checkout main`, { cwd: cloneDir });
211
+ await execAsync(`git reset --hard origin/main`, { cwd: cloneDir });
212
+ });
213
+
214
+ test('branch that never existed on the remote -> clear BRANCH_NOT_FOUND', async () => {
215
+ await assert.rejects(
216
+ () => fetchRequestedRef((cmd, opts) => execAsync(cmd, opts), 'no-such-branch', cloneDir),
217
+ (err) => {
218
+ assert.equal(err.code, 'BRANCH_NOT_FOUND');
219
+ assert.equal(err.message, 'branch not found on remote: no-such-branch');
220
+ return true;
221
+ }
222
+ );
223
+ });
224
+ });
@@ -0,0 +1,60 @@
1
+ import { test } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { BurstSuppressor, normalize, suppressionKey } from '../src/siblings/burst-suppressor.js';
4
+
5
+ const evt = (message, over = {}) => ({
6
+ adapter: 'sap', errorCode: 'E', file: '/logs/dev_w0',
7
+ message, source: 'sap-01', timestamp: '2026-07-28T10:00:00Z', ...over,
8
+ });
9
+
10
+ test('normalize collapses rotating ids; raw values differ', () => {
11
+ const a = 'DB timeout id=10001 uuid=1b4e28ba-2fa1-11d2-883f-0016d3cca427 at 2026-07-28T10:00:01Z addr=0x1f3a /job/req-9001';
12
+ const b = 'DB timeout id=99887 uuid=6ecd8c99-4036-403d-bf84-cf8400f67836 at 2026-07-28T11:22:33.5Z addr=0xbeef /job/req-4444';
13
+ assert.notEqual(a, b);
14
+ assert.equal(normalize(a), normalize(b)); // rotating tokens -> same placeholders
15
+ });
16
+
17
+ test('WITHOUT normalization the storm keys are all distinct (suppresses nothing)', () => {
18
+ // Proves normalization is what makes suppression work.
19
+ const rawKeys = new Set();
20
+ for (let i = 0; i < 500; i++) rawKeys.add(`sap|DB timeout id=${10000 + i}|/logs/dev_w0`); // raw, no normalize
21
+ assert.equal(rawKeys.size, 500);
22
+ // Same inputs through suppressionKey (which normalizes) collapse to one.
23
+ const normKeys = new Set();
24
+ for (let i = 0; i < 500; i++) normKeys.add(suppressionKey(evt(`DB timeout id=${10000 + i}`)));
25
+ assert.equal(normKeys.size, 1);
26
+ });
27
+
28
+ test('storm of 500 variants collapses to 1 within the window', () => {
29
+ let t = 1_000_000;
30
+ const s = new BurstSuppressor({ ttlMs: 45000, now: () => t });
31
+ let passed = 0;
32
+ for (let i = 0; i < 500; i++) {
33
+ t += 10; // 5s total, all within the 45s window
34
+ if (!s.shouldSuppress(evt(`DB timeout id=${10000 + i}`))) passed++;
35
+ }
36
+ assert.equal(passed, 1);
37
+ });
38
+
39
+ test('two genuinely distinct errors both pass', () => {
40
+ const s = new BurstSuppressor({ ttlMs: 45000, now: () => 5 });
41
+ assert.equal(s.shouldSuppress(evt('disk full on /data')), false);
42
+ assert.equal(s.shouldSuppress(evt('connection refused to db')), false);
43
+ });
44
+
45
+ test('same error passes again after TTL expiry', () => {
46
+ let t = 0;
47
+ const s = new BurstSuppressor({ ttlMs: 45000, now: () => t });
48
+ assert.equal(s.shouldSuppress(evt('DB timeout id=10001')), false); // admit
49
+ t = 20000;
50
+ assert.equal(s.shouldSuppress(evt('DB timeout id=10002')), true); // within window -> suppress
51
+ t = 46000; // window elapsed from admission
52
+ assert.equal(s.shouldSuppress(evt('DB timeout id=10003')), false); // passes again (recurrence signal)
53
+ });
54
+
55
+ test('bounded LRU map does not grow unbounded', () => {
56
+ let t = 0;
57
+ const s = new BurstSuppressor({ ttlMs: 45000, maxKeys: 100, now: () => (t += 1) });
58
+ for (let i = 0; i < 1000; i++) s.shouldSuppress(evt(`unique error number ${i} zzz`, { file: `/f/${i}` }));
59
+ assert.ok(s.seen.size <= 100, `size ${s.seen.size} must be <= 100`);
60
+ });
@@ -0,0 +1,47 @@
1
+ import { test } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { isAuthorized, publicCapabilities, fullCapabilities } from '../src/siblings/capabilities.js';
4
+
5
+ const MANIFEST = {
6
+ mode: 'SPLIT_PLANE', reason: 'partial local capability',
7
+ source_available: false, toolchains: ['git', 'python3'],
8
+ container_runtime: 'docker', docker_sock: false, egress: { gateway: true },
9
+ build_capable: false, test_capable: false, runtimes: [],
10
+ };
11
+ const STATUS = { contract_ok: false, contract_preflight: { ok: false, reason: 'missing_internal_token' }, stats: {} };
12
+
13
+ const SENSITIVE = ['toolchains', 'container_runtime', 'docker_sock', 'egress', 'source_available'];
14
+
15
+ test('unauthenticated view exposes health only, no topology inventory', () => {
16
+ const v = publicCapabilities(MANIFEST, STATUS, 42);
17
+ assert.equal(v.authenticated, false);
18
+ assert.equal(v.mode, 'SPLIT_PLANE');
19
+ assert.equal(v.contract.ok, false);
20
+ assert.equal(v.contract.preflight_reason, 'missing_internal_token'); // the C1 signal
21
+ assert.equal(v.uptime_s, 42);
22
+ for (const k of SENSITIVE) assert.ok(!(k in v), `${k} must be redacted when unauthenticated`);
23
+ });
24
+
25
+ test('authenticated view adds the client-infra inventory', () => {
26
+ const v = fullCapabilities(MANIFEST, STATUS, 42);
27
+ assert.equal(v.authenticated, true);
28
+ for (const k of SENSITIVE) assert.ok(k in v, `${k} must be present when authenticated`);
29
+ assert.deepEqual(v.toolchains, ['git', 'python3']);
30
+ assert.equal(v.container_runtime, 'docker');
31
+ // health still present
32
+ assert.equal(v.mode, 'SPLIT_PLANE');
33
+ assert.equal(v.contract.ok, false);
34
+ });
35
+
36
+ test('isAuthorized: correct bearer passes, wrong/absent fails', () => {
37
+ const tok = 'AGENT_API_KEY_123';
38
+ assert.equal(isAuthorized({ headers: { authorization: `Bearer ${tok}` } }, tok), true);
39
+ assert.equal(isAuthorized({ headers: { authorization: 'Bearer wrong' } }, tok), false);
40
+ assert.equal(isAuthorized({ headers: {} }, tok), false);
41
+ assert.equal(isAuthorized({ headers: { authorization: tok } }, tok), false); // missing "Bearer "
42
+ });
43
+
44
+ test('isAuthorized fails closed when no token is configured', () => {
45
+ assert.equal(isAuthorized({ headers: { authorization: 'Bearer anything' } }, null), false);
46
+ assert.equal(isAuthorized({ headers: { authorization: 'Bearer anything' } }, ''), false);
47
+ });
@@ -0,0 +1,87 @@
1
+ import { test } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import fs from 'node:fs';
4
+ import path from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+ import { buildManifest, deriveMode, computeCapabilityManifest } from '../src/siblings/capabilities.js';
7
+
8
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
9
+ const loadFixture = (name) =>
10
+ JSON.parse(fs.readFileSync(path.join(__dirname, 'fixtures', 'capabilities', name), 'utf-8'));
11
+
12
+ test('pull-only Docker host derives SPLIT_PLANE, not FULL_LOCAL', () => {
13
+ const fx = loadFixture('pull-only-docker.json');
14
+ const m = buildManifest(fx);
15
+
16
+ assert.equal(m.mode, 'SPLIT_PLANE');
17
+ assert.notEqual(m.mode, 'FULL_LOCAL');
18
+ // Evidence the fixture was interpreted as intended:
19
+ assert.equal(m.source_available, false);
20
+ assert.equal(m.build_capable, false);
21
+ assert.equal(m.test_capable, false);
22
+ assert.deepEqual(m.toolchains.sort(), ['git', 'python3']);
23
+ assert.match(m.reason, /no source working tree/);
24
+ });
25
+
26
+ test('full toolchain + source on disk derives FULL_LOCAL', () => {
27
+ const m = buildManifest({
28
+ diag: { tools: {
29
+ git: { installed: true }, java: { installed: true }, maven: { installed: true },
30
+ node: { installed: true }, npm: { installed: true },
31
+ } },
32
+ sourceAvailable: true,
33
+ egress: { gateway: true },
34
+ conclusive: true,
35
+ });
36
+ assert.equal(m.mode, 'FULL_LOCAL');
37
+ assert.equal(m.build_capable, true);
38
+ assert.equal(m.test_capable, true);
39
+ });
40
+
41
+ test('inconclusive detection NEVER defaults to FULL_LOCAL', () => {
42
+ const m = buildManifest({
43
+ diag: { tools: { java: { installed: true }, maven: { installed: true }, git: { installed: true } } },
44
+ sourceAvailable: true, // even with "everything", inconclusive must degrade
45
+ egress: { gateway: true },
46
+ conclusive: false,
47
+ });
48
+ assert.equal(m.mode, 'DETECT_ONLY');
49
+ assert.match(m.reason, /inconclusive/);
50
+ });
51
+
52
+ test('no source, no toolchain, no git/egress derives DETECT_ONLY', () => {
53
+ const { mode } = deriveMode({
54
+ source_available: false, build_capable: false, test_capable: false,
55
+ hasGit: false, egress: {}, conclusive: true,
56
+ });
57
+ assert.equal(mode, 'DETECT_ONLY');
58
+ });
59
+
60
+ test('computeCapabilityManifest degrades to DETECT_ONLY when diagnostics throw', async () => {
61
+ const throwingDiagnostics = { diagnose: async () => { throw new Error('boom'); } };
62
+ const m = await computeCapabilityManifest({
63
+ diagnostics: throwingDiagnostics,
64
+ runtimeRegistry: { runtimes: new Map() },
65
+ probes: { sourceAvailable: true, dockerSock: false, containerRuntime: 'docker', egress: { gateway: true } },
66
+ });
67
+ assert.equal(m.mode, 'DETECT_ONLY');
68
+ assert.match(m.reason, /inconclusive/);
69
+ });
70
+
71
+ test('computeCapabilityManifest honours injected probes + diagnostics (pull-only)', async () => {
72
+ const fx = loadFixture('pull-only-docker.json');
73
+ const diagnostics = { diagnose: async () => fx.diag };
74
+ const m = await computeCapabilityManifest({
75
+ diagnostics,
76
+ runtimeRegistry: { runtimes: new Map() },
77
+ probes: {
78
+ sourceAvailable: fx.sourceAvailable,
79
+ dockerSock: fx.dockerSock,
80
+ containerRuntime: fx.containerRuntime,
81
+ egress: fx.egress,
82
+ },
83
+ });
84
+ assert.equal(m.mode, 'SPLIT_PLANE');
85
+ assert.equal(m.container_runtime, 'docker');
86
+ assert.equal(m.docker_sock, false);
87
+ });
@@ -0,0 +1,23 @@
1
+ // test/child-env.test.js — model-authored commands must not see the agent's secrets.
2
+ import { test, describe } from 'node:test';
3
+ import assert from 'node:assert/strict';
4
+ import { sanitizedChildEnv, isSecretEnvName } from '../src/utils/child-env.js';
5
+
6
+ describe('sanitizedChildEnv', () => {
7
+ test('strips token/secret/password/key variables, keeps the rest and extras', () => {
8
+ const env = {
9
+ PATH: '/usr/bin', HOME: '/home/x', JAVA_HOME: '/opt/java',
10
+ AUTH_TOKEN: 'a', DEEPDEBUG_API_KEY: 'b', AGENT_SHARED_SECRET: 'c', LOCAL_AGENT_TOKEN: 'd',
11
+ GITHUB_TOKEN: 'e', DB_PASSWORD: 'f', AWS_SECRET_ACCESS_KEY: 'g', LOCAL_AGENT_SIGNING_KEY: 'h',
12
+ GIT_TERMINAL_PROMPT: '0', GIT_ASKPASS: 'echo', UNDEFINED: undefined,
13
+ };
14
+ const out = sanitizedChildEnv(env, { FORCE_COLOR: '0' });
15
+ assert.deepEqual(out, { PATH: '/usr/bin', HOME: '/home/x', JAVA_HOME: '/opt/java', GIT_TERMINAL_PROMPT: '0', GIT_ASKPASS: 'echo', FORCE_COLOR: '0' });
16
+ });
17
+ test('isSecretEnvName', () => {
18
+ assert.equal(isSecretEnvName('NPM_TOKEN'), true);
19
+ assert.equal(isSecretEnvName('npm_config_registry'), false);
20
+ assert.equal(isSecretEnvName('GIT_SSH_COMMAND'), false);
21
+ assert.equal(isSecretEnvName('MAVEN_OPTS'), false);
22
+ });
23
+ });