quadwork 1.19.2 → 2.0.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 (117) hide show
  1. package/README.md +19 -35
  2. package/bin/quadwork.js +48 -1118
  3. package/out/404.html +1 -1
  4. package/out/__next.__PAGE__.txt +3 -3
  5. package/out/__next._full.txt +14 -14
  6. package/out/__next._head.txt +4 -4
  7. package/out/__next._index.txt +8 -8
  8. package/out/__next._tree.txt +2 -2
  9. package/out/_next/static/chunks/{030cjkhts487t.js → 079wdniva~de1.js} +1 -1
  10. package/out/_next/static/chunks/{0n~dq4kpx9xxx.js → 07lhk_q6pmm3r.js} +1 -1
  11. package/out/_next/static/chunks/0_79hkefw1mo2.js +1 -0
  12. package/out/_next/static/chunks/{153f.fj8jlvle.js → 0_lyyn..t63bc.js} +1 -1
  13. package/out/_next/static/chunks/0oxv9vrvc17to.js +2 -0
  14. package/out/_next/static/chunks/0py7102i226n5.js +1 -0
  15. package/out/_next/static/chunks/{13fv-yi7.v52g.js → 0q4bm04c1jl_3.js} +1 -1
  16. package/out/_next/static/chunks/{0_idxioyl0p7h.js → 0sjhy6oe3mbon.js} +1 -1
  17. package/out/_next/static/chunks/13xk0vgfbrcld.css +2 -0
  18. package/out/_next/static/chunks/14k3bfe537f9_.js +25 -0
  19. package/out/_next/static/chunks/{turbopack-0qm-e3ifrz~2u.js → turbopack-0y2u-q0l2m67w.js} +1 -1
  20. package/out/_not-found/__next._full.txt +13 -13
  21. package/out/_not-found/__next._head.txt +4 -4
  22. package/out/_not-found/__next._index.txt +8 -8
  23. package/out/_not-found/__next._not-found.__PAGE__.txt +2 -2
  24. package/out/_not-found/__next._not-found.txt +3 -3
  25. package/out/_not-found/__next._tree.txt +2 -2
  26. package/out/_not-found.html +1 -1
  27. package/out/_not-found.txt +13 -13
  28. package/out/app-shell/__next._full.txt +13 -13
  29. package/out/app-shell/__next._head.txt +4 -4
  30. package/out/app-shell/__next._index.txt +8 -8
  31. package/out/app-shell/__next._tree.txt +2 -2
  32. package/out/app-shell/__next.app-shell.__PAGE__.txt +2 -2
  33. package/out/app-shell/__next.app-shell.txt +3 -3
  34. package/out/app-shell.html +1 -1
  35. package/out/app-shell.txt +13 -13
  36. package/out/index.html +1 -1
  37. package/out/index.txt +14 -14
  38. package/out/project/_/__next._full.txt +14 -14
  39. package/out/project/_/__next._head.txt +4 -4
  40. package/out/project/_/__next._index.txt +8 -8
  41. package/out/project/_/__next._tree.txt +2 -2
  42. package/out/project/_/__next.project.$d$id.__PAGE__.txt +3 -3
  43. package/out/project/_/__next.project.$d$id.txt +3 -3
  44. package/out/project/_/__next.project.txt +3 -3
  45. package/out/project/_/queue/__next._full.txt +14 -14
  46. package/out/project/_/queue/__next._head.txt +4 -4
  47. package/out/project/_/queue/__next._index.txt +8 -8
  48. package/out/project/_/queue/__next._tree.txt +2 -2
  49. package/out/project/_/queue/__next.project.$d$id.queue.__PAGE__.txt +3 -3
  50. package/out/project/_/queue/__next.project.$d$id.queue.txt +3 -3
  51. package/out/project/_/queue/__next.project.$d$id.txt +3 -3
  52. package/out/project/_/queue/__next.project.txt +3 -3
  53. package/out/project/_/queue.html +1 -1
  54. package/out/project/_/queue.txt +14 -14
  55. package/out/project/_.html +1 -1
  56. package/out/project/_.txt +14 -14
  57. package/out/settings/__next._full.txt +14 -14
  58. package/out/settings/__next._head.txt +4 -4
  59. package/out/settings/__next._index.txt +8 -8
  60. package/out/settings/__next._tree.txt +2 -2
  61. package/out/settings/__next.settings.__PAGE__.txt +3 -3
  62. package/out/settings/__next.settings.txt +3 -3
  63. package/out/settings.html +1 -1
  64. package/out/settings.txt +14 -14
  65. package/out/setup/__next._full.txt +14 -14
  66. package/out/setup/__next._head.txt +4 -4
  67. package/out/setup/__next._index.txt +8 -8
  68. package/out/setup/__next._tree.txt +2 -2
  69. package/out/setup/__next.setup.__PAGE__.txt +3 -3
  70. package/out/setup/__next.setup.txt +3 -3
  71. package/out/setup.html +1 -1
  72. package/out/setup.txt +14 -14
  73. package/package.json +4 -2
  74. package/server/ac-restore.js +128 -0
  75. package/server/bridges/discord.js +183 -0
  76. package/server/bridges/telegram.js +210 -0
  77. package/server/config.js +4 -60
  78. package/server/file-chat.js +318 -0
  79. package/server/index.js +173 -1286
  80. package/server/install-agentchattr.js +3 -284
  81. package/server/mcp-chat-shim.js +171 -0
  82. package/server/migrate-ac.js +158 -0
  83. package/server/pty-dispatcher.js +188 -0
  84. package/server/routes.js +149 -1397
  85. package/templates/CLAUDE.md +2 -2
  86. package/templates/OVERNIGHT-QUEUE.md +1 -1
  87. package/templates/seeds/butler.CLAUDE.md +30 -62
  88. package/templates/seeds/dev.AGENTS.md +10 -1
  89. package/templates/seeds/head.AGENTS.md +3 -3
  90. package/templates/seeds/re1.AGENTS.md +3 -3
  91. package/templates/seeds/re2.AGENTS.md +3 -3
  92. package/bridges/discord/__pycache__/discord_bridge.cpython-314.pyc +0 -0
  93. package/bridges/discord/discord_bridge.py +0 -666
  94. package/bridges/discord/requirements.txt +0 -2
  95. package/out/_next/static/chunks/0_bb~2.5h2ntm.css +0 -2
  96. package/out/_next/static/chunks/0makcdqkwobp6.js +0 -25
  97. package/out/_next/static/chunks/0uz5svjlo9dwl.js +0 -1
  98. package/out/_next/static/chunks/0zahstmgdrpy5.js +0 -1
  99. package/out/_next/static/chunks/0zfotsowwll1x.js +0 -2
  100. package/server/__tests__/bridge-auto-stop-guard.test.js +0 -134
  101. package/server/__tests__/rate-limit-handling.test.js +0 -168
  102. package/server/__tests__/scrub-secrets.test.js +0 -235
  103. package/server/__tests__/v1110-security-qa.test.js +0 -312
  104. package/server/agentchattr-registry.js +0 -188
  105. package/server/install-agentchattr.patchCrashTimeout.test.js +0 -71
  106. package/server/queue-watcher.js +0 -171
  107. package/server/queue-watcher.test.js +0 -64
  108. package/server/routes.batchProgress.test.js +0 -94
  109. package/server/routes.chatWsSend.test.js +0 -161
  110. package/server/routes.discordBridge.test.js +0 -80
  111. package/server/routes.parseActiveBatch.test.js +0 -88
  112. package/server/routes.telegramBridge.test.js +0 -241
  113. package/templates/config.toml +0 -72
  114. package/templates/wrapper.py +0 -70
  115. /package/out/_next/static/{K7A3YZrh4sLaRRP1-Lq7v → 479UD5Kit4YvCmtgO25VT}/_buildManifest.js +0 -0
  116. /package/out/_next/static/{K7A3YZrh4sLaRRP1-Lq7v → 479UD5Kit4YvCmtgO25VT}/_clientMiddlewareManifest.js +0 -0
  117. /package/out/_next/static/{K7A3YZrh4sLaRRP1-Lq7v → 479UD5Kit4YvCmtgO25VT}/_ssgManifest.js +0 -0
@@ -1,134 +0,0 @@
1
- /**
2
- * #542 — Verify bridge auto-stop fires only on the transition from
3
- * incomplete → complete, not on every polling tick.
4
- *
5
- * Since autoStopPollingTick is tightly coupled to HTTP calls, we test
6
- * the guard logic by reading server/index.js and verifying the code
7
- * structure, then simulating the guard in isolation.
8
- */
9
-
10
- const fs = require("fs");
11
- const path = require("path");
12
-
13
- const SERVER_PATH = path.join(__dirname, "..", "index.js");
14
- const src = fs.readFileSync(SERVER_PATH, "utf-8");
15
-
16
- // ---------------------------------------------------------------------------
17
- // 1. Code-structure assertions — verify the guard exists in source
18
- // ---------------------------------------------------------------------------
19
-
20
- describe("#542 bridge auto-stop transition guard (code analysis)", () => {
21
- test("autoStopBridges in polling path is guarded by prev.complete check", () => {
22
- // Find the autoStopPollingTick function body
23
- const fnStart = src.indexOf("async function autoStopPollingTick()");
24
- expect(fnStart).toBeGreaterThan(-1);
25
-
26
- // Extract the function body (up to the next top-level function or setInterval)
27
- const fnBody = src.slice(fnStart, src.indexOf("setInterval(autoStopPollingTick"));
28
-
29
- // The autoStopBridges call must be guarded by a prev.complete check
30
- const stopIdx = fnBody.indexOf("autoStopBridges(project.id");
31
- expect(stopIdx).toBeGreaterThan(-1);
32
-
33
- // Look at the surrounding context — the guard should appear before the call
34
- const guardRegion = fnBody.slice(Math.max(0, stopIdx - 200), stopIdx);
35
- expect(guardRegion).toMatch(/!prev\.complete/);
36
- });
37
-
38
- test("autoStartBridges in polling path is still transition-guarded", () => {
39
- const fnStart = src.indexOf("async function autoStopPollingTick()");
40
- const fnBody = src.slice(fnStart, src.indexOf("setInterval(autoStopPollingTick"));
41
- const startIdx = fnBody.indexOf("autoStartBridges(");
42
- expect(startIdx).toBeGreaterThan(-1);
43
-
44
- const guardRegion = fnBody.slice(Math.max(0, startIdx - 200), startIdx);
45
- expect(guardRegion).toMatch(/isNewBatch/);
46
- });
47
-
48
- test("_bridgeBatchPrev.set is called before the guard checks", () => {
49
- const fnStart = src.indexOf("async function autoStopPollingTick()");
50
- const fnBody = src.slice(fnStart, src.indexOf("setInterval(autoStopPollingTick"));
51
-
52
- const setIdx = fnBody.indexOf("_bridgeBatchPrev.set(");
53
- const stopIdx = fnBody.indexOf("autoStopBridges(project.id");
54
- expect(setIdx).toBeGreaterThan(-1);
55
- expect(setIdx).toBeLessThan(stopIdx);
56
- });
57
-
58
- test("autoStopBridges in sendTriggerMessage is guarded by prev.complete check", () => {
59
- const fnStart = src.indexOf("async function sendTriggerMessage(");
60
- expect(fnStart).toBeGreaterThan(-1);
61
-
62
- // Extract until the next top-level function
63
- const fnBody = src.slice(fnStart, fnStart + 2000);
64
-
65
- const stopIdx = fnBody.indexOf("autoStopBridges(");
66
- expect(stopIdx).toBeGreaterThan(-1);
67
-
68
- // The guard should appear before the call
69
- const guardRegion = fnBody.slice(Math.max(0, stopIdx - 300), stopIdx);
70
- expect(guardRegion).toMatch(/!prev\.complete/);
71
- });
72
-
73
- test("sendTriggerMessage updates _bridgeBatchPrev before the guard", () => {
74
- const fnStart = src.indexOf("async function sendTriggerMessage(");
75
- const fnBody = src.slice(fnStart, fnStart + 2000);
76
-
77
- const setIdx = fnBody.indexOf("_bridgeBatchPrev.set(");
78
- const stopIdx = fnBody.indexOf("autoStopBridges(");
79
- expect(setIdx).toBeGreaterThan(-1);
80
- expect(setIdx).toBeLessThan(stopIdx);
81
- });
82
- });
83
-
84
- // ---------------------------------------------------------------------------
85
- // 2. Guard-logic simulation — mirrors the if-condition in autoStopPollingTick
86
- // ---------------------------------------------------------------------------
87
-
88
- describe("#542 bridge auto-stop transition guard (logic simulation)", () => {
89
- function shouldAutoStop(bp, prev) {
90
- // Mirrors the production guard:
91
- // if (hasBridgeAuto && (!prev || !prev.complete)) { autoStopBridges(...) }
92
- if (!(bp && bp.complete)) return false;
93
- return !prev || !prev.complete;
94
- }
95
-
96
- test("fires on first tick when batch is already complete (no prev)", () => {
97
- expect(shouldAutoStop({ complete: true }, undefined)).toBe(true);
98
- });
99
-
100
- test("fires on transition from incomplete to complete", () => {
101
- expect(shouldAutoStop({ complete: true }, { complete: false, hasItems: true })).toBe(true);
102
- });
103
-
104
- test("does NOT fire on repeated complete ticks", () => {
105
- expect(shouldAutoStop({ complete: true }, { complete: true, hasItems: true })).toBe(false);
106
- });
107
-
108
- test("does NOT fire when batch is not complete", () => {
109
- expect(shouldAutoStop({ complete: false }, undefined)).toBe(false);
110
- expect(shouldAutoStop({ complete: false }, { complete: false })).toBe(false);
111
- });
112
-
113
- test("manually restarted bridges survive repeated complete ticks", () => {
114
- // Simulates: batch completes → tick 1 stops bridges → operator restarts →
115
- // tick 2 should NOT stop again because prev.complete is already true
116
- const tick1 = shouldAutoStop({ complete: true }, undefined);
117
- expect(tick1).toBe(true); // first transition fires
118
-
119
- // After tick 1, prev = { complete: true }
120
- const tick2 = shouldAutoStop({ complete: true }, { complete: true });
121
- expect(tick2).toBe(false); // no repeated stop
122
- });
123
-
124
- test("auto-stop fires again for a new batch cycle", () => {
125
- // batch 1 completes → new batch starts → new batch completes
126
- const batch1Complete = shouldAutoStop({ complete: true }, { complete: false });
127
- expect(batch1Complete).toBe(true);
128
-
129
- // new batch is in progress (prev was complete from batch 1)
130
- // then new batch completes
131
- const batch2Complete = shouldAutoStop({ complete: true }, { complete: false });
132
- expect(batch2Complete).toBe(true);
133
- });
134
- });
@@ -1,168 +0,0 @@
1
- /**
2
- * #554 — Verify rate-limit-aware caching and backoff in server/routes.js.
3
- *
4
- * Tests verify:
5
- * 1. Rate limit state variables exist and are initialised
6
- * 2. adaptiveTTL returns extended TTLs when rate is low/critical
7
- * 3. cachedGhEndpoint serves stale data with _rateLimited flag
8
- * 4. GitHub endpoints use the cached helper
9
- * 5. /api/github/rate-limit endpoint is registered
10
- * 6. Batch progress handler has rate-limit guard
11
- */
12
-
13
- const fs = require("fs");
14
- const path = require("path");
15
-
16
- const ROUTES_PATH = path.join(__dirname, "..", "routes.js");
17
- const src = fs.readFileSync(ROUTES_PATH, "utf-8");
18
-
19
- // ---------------------------------------------------------------------------
20
- // 1. Rate limit state and constants
21
- // ---------------------------------------------------------------------------
22
-
23
- describe("#554 rate limit infrastructure (code analysis)", () => {
24
- test("_rateLimit state object is defined with expected fields", () => {
25
- expect(src).toContain("const _rateLimit = {");
26
- expect(src).toContain("remaining:");
27
- expect(src).toContain("resetAt:");
28
- });
29
-
30
- test("RATE_LIMIT_LOW_THRESHOLD and RATE_LIMIT_CRITICAL are defined", () => {
31
- expect(src).toMatch(/RATE_LIMIT_LOW_THRESHOLD\s*=\s*\d+/);
32
- expect(src).toMatch(/RATE_LIMIT_CRITICAL\s*=\s*\d+/);
33
- });
34
-
35
- test("refreshRateLimit calls gh api rate_limit", () => {
36
- expect(src).toContain("gh");
37
- expect(src).toContain("api");
38
- expect(src).toContain("rate_limit");
39
- });
40
-
41
- test("startRateLimitPolling is called at module load", () => {
42
- expect(src).toContain("startRateLimitPolling()");
43
- });
44
- });
45
-
46
- // ---------------------------------------------------------------------------
47
- // 2. Adaptive TTL
48
- // ---------------------------------------------------------------------------
49
-
50
- describe("#554 adaptive TTL logic", () => {
51
- test("adaptiveTTL function is defined", () => {
52
- expect(src).toContain("function adaptiveTTL(baseTTL)");
53
- });
54
-
55
- test("adaptiveTTL returns Infinity when critically rate-limited", () => {
56
- expect(src).toMatch(/isRateLimited\(\).*Infinity/s);
57
- });
58
-
59
- test("adaptiveTTL extends TTL when rate is low", () => {
60
- expect(src).toMatch(/isRateLow\(\).*120[_,]?000/s);
61
- });
62
- });
63
-
64
- // ---------------------------------------------------------------------------
65
- // 3. Cached endpoint helper
66
- // ---------------------------------------------------------------------------
67
-
68
- describe("#554 cachedGhEndpoint helper", () => {
69
- test("cachedGhEndpoint function is defined", () => {
70
- expect(src).toContain("function cachedGhEndpoint(");
71
- });
72
-
73
- test("serves stale data with _rateLimited flag when critical", () => {
74
- const fnStart = src.indexOf("function cachedGhEndpoint(");
75
- const fnBody = src.slice(fnStart, fnStart + 800);
76
- expect(fnBody).toContain("_rateLimited");
77
- expect(fnBody).toContain("_stale");
78
- });
79
-
80
- test("uses adaptiveTTL for cache checks", () => {
81
- const fnStart = src.indexOf("function cachedGhEndpoint(");
82
- const fnBody = src.slice(fnStart, fnStart + 600);
83
- expect(fnBody).toContain("adaptiveTTL");
84
- });
85
- });
86
-
87
- // ---------------------------------------------------------------------------
88
- // 4. GitHub endpoints use cached helper
89
- // ---------------------------------------------------------------------------
90
-
91
- describe("#554 GitHub endpoints use cachedGhEndpoint", () => {
92
- test("/api/github/issues uses cachedGhEndpoint", () => {
93
- const section = src.slice(
94
- src.indexOf('"/api/github/issues"'),
95
- src.indexOf('"/api/github/issues"') + 300,
96
- );
97
- expect(section).toContain("cachedGhEndpoint");
98
- });
99
-
100
- test("/api/github/prs uses cachedGhEndpoint", () => {
101
- const section = src.slice(
102
- src.indexOf('"/api/github/prs"'),
103
- src.indexOf('"/api/github/prs"') + 300,
104
- );
105
- expect(section).toContain("cachedGhEndpoint");
106
- });
107
-
108
- test("/api/github/closed-issues uses cachedGhEndpoint", () => {
109
- const section = src.slice(
110
- src.indexOf('"/api/github/closed-issues"'),
111
- src.indexOf('"/api/github/closed-issues"') + 500,
112
- );
113
- expect(section).toContain("cachedGhEndpoint");
114
- });
115
-
116
- test("/api/github/merged-prs uses cachedGhEndpoint", () => {
117
- const section = src.slice(
118
- src.indexOf('"/api/github/merged-prs"'),
119
- src.indexOf('"/api/github/merged-prs"') + 500,
120
- );
121
- expect(section).toContain("cachedGhEndpoint");
122
- });
123
- });
124
-
125
- // ---------------------------------------------------------------------------
126
- // 5. Rate limit API endpoint
127
- // ---------------------------------------------------------------------------
128
-
129
- describe("#554 /api/github/rate-limit endpoint", () => {
130
- test("endpoint is registered", () => {
131
- expect(src).toContain('"/api/github/rate-limit"');
132
- });
133
-
134
- test("returns remaining, limit, resetInMinutes, low, critical fields", () => {
135
- const idx = src.indexOf('"/api/github/rate-limit"');
136
- const section = src.slice(idx, idx + 500);
137
- expect(section).toContain("remaining");
138
- expect(section).toContain("limit");
139
- expect(section).toContain("resetInMinutes");
140
- expect(section).toContain("low");
141
- expect(section).toContain("critical");
142
- });
143
- });
144
-
145
- // ---------------------------------------------------------------------------
146
- // 6. Batch progress rate limit guard
147
- // ---------------------------------------------------------------------------
148
-
149
- describe("#554 batch progress rate limit awareness", () => {
150
- test("batch progress handler checks isRateLimited before gh calls", () => {
151
- const batchStart = src.indexOf('"/api/batch-progress"');
152
- const batchSection = src.slice(batchStart, batchStart + 600);
153
- expect(batchSection).toContain("isRateLimited()");
154
- expect(batchSection).toContain("_rateLimited");
155
- });
156
-
157
- test("batch progress uses adaptiveTTL for cache", () => {
158
- const batchStart = src.indexOf('"/api/batch-progress"');
159
- const batchSection = src.slice(batchStart, batchStart + 400);
160
- expect(batchSection).toContain("adaptiveTTL");
161
- });
162
-
163
- test("projects endpoint uses adaptiveTTL for cache", () => {
164
- const projStart = src.indexOf('"/api/projects"');
165
- const projSection = src.slice(projStart, projStart + 400);
166
- expect(projSection).toContain("adaptiveTTL");
167
- });
168
- });
@@ -1,235 +0,0 @@
1
- /**
2
- * #545: QA verification for #538 PTY scrollback secret scrubbing.
3
- *
4
- * Tests the production scrubSecrets / scrubScrollback from server/scrub-secrets.js:
5
- * - Redact secrets correctly (SECRET_NAME, API key prefixes, Bearer)
6
- * - Pass through normal terminal output unchanged
7
- * - Preserve ANSI escape codes on non-secret lines
8
- * - Handle edge cases (empty input, very long lines, rapid multi-line output)
9
- *
10
- * Integration-level checklist items (resize, reconnect/replay flow,
11
- * multi-agent isolation, two-tab behavior) are verified by code-path
12
- * analysis in the PR description — they depend on WS + PTY runtime
13
- * that cannot be unit-tested without a full server harness.
14
- */
15
-
16
- const { scrubSecrets, scrubScrollback, _REDACTED } = require("../scrub-secrets");
17
-
18
- // ---- Tests ----
19
-
20
- const assert = require("assert");
21
- let passed = 0;
22
- let failed = 0;
23
-
24
- function test(name, fn) {
25
- try {
26
- fn();
27
- passed++;
28
- console.log(` PASS ${name}`);
29
- } catch (e) {
30
- failed++;
31
- console.error(` FAIL ${name}`);
32
- console.error(` ${e.message}`);
33
- }
34
- }
35
-
36
- console.log("\n#545 QA — scrubSecrets / scrubScrollback (production module)\n");
37
-
38
- // --- 1. Secret redaction (true positives) ---
39
-
40
- test("redacts ANTHROPIC_API_KEY=value", () => {
41
- const input = "ANTHROPIC_API_KEY=sk-ant-api03-xxxxxxxxxxxx";
42
- const out = scrubSecrets(input);
43
- assert(!out.includes("sk-ant-api03"), `got: ${out}`);
44
- assert(out.includes(_REDACTED));
45
- });
46
-
47
- test("redacts DB_PASSWORD: hunter2", () => {
48
- const out = scrubSecrets("DB_PASSWORD: hunter2");
49
- assert(!out.includes("hunter2"), `got: ${out}`);
50
- });
51
-
52
- test("redacts GITHUB_TOKEN=ghp_...", () => {
53
- const token = "ghp_" + "A".repeat(36);
54
- const out = scrubSecrets(`export GITHUB_TOKEN=${token}`);
55
- assert(!out.includes(token), `got: ${out}`);
56
- });
57
-
58
- test("redacts Bearer token header", () => {
59
- const out = scrubSecrets("Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.payload.sig");
60
- assert(!out.includes("eyJhbG"), `got: ${out}`);
61
- assert(out.includes(_REDACTED));
62
- });
63
-
64
- test("redacts standalone Bearer (no Authorization: prefix)", () => {
65
- const out = scrubSecrets("curl -H 'Bearer eyJhbGciOiJIUzI1NiJ9.xxxxxxxxxxxx'");
66
- assert(!out.includes("eyJhbG"), `got: ${out}`);
67
- assert(out.includes(`Bearer ${_REDACTED}`));
68
- });
69
-
70
- test("redacts sk- OpenAI key inline", () => {
71
- const key = "sk-" + "a".repeat(48);
72
- const out = scrubSecrets(`Using key ${key} for request`);
73
- assert(!out.includes(key), `got: ${out}`);
74
- });
75
-
76
- test("redacts Slack xoxb token", () => {
77
- const token = "xoxb-" + "1234567890-" + "A".repeat(20);
78
- const out = scrubSecrets(`SLACK_TOKEN=${token}`);
79
- assert(!out.includes(token), `got: ${out}`);
80
- });
81
-
82
- // --- 2. Normal output passes through unchanged ---
83
-
84
- test("preserves plain text", () => {
85
- const input = "Hello, world! This is normal output.";
86
- assert.strictEqual(scrubSecrets(input), input);
87
- });
88
-
89
- test("preserves npm install output", () => {
90
- const input = "added 1423 packages in 45s\n182 packages are looking for funding";
91
- assert.strictEqual(scrubSecrets(input), input);
92
- });
93
-
94
- test("preserves git clone output", () => {
95
- const input = "Cloning into 'repo'...\nremote: Enumerating objects: 1234, done.\nReceiving objects: 100% (1234/1234)";
96
- assert.strictEqual(scrubSecrets(input), input);
97
- });
98
-
99
- test("preserves test runner output", () => {
100
- const input = "Tests: 42 passed, 42 total\nTime: 3.456 s";
101
- assert.strictEqual(scrubSecrets(input), input);
102
- });
103
-
104
- test("preserves JSON output without secrets", () => {
105
- const input = '{"status":"ok","count":42,"items":["a","b","c"]}';
106
- assert.strictEqual(scrubSecrets(input), input);
107
- });
108
-
109
- // --- 3. ANSI escape code handling ---
110
-
111
- test("preserves ANSI colors on non-secret lines", () => {
112
- const input = "\x1b[32m✓\x1b[0m test passed";
113
- assert.strictEqual(scrubSecrets(input), input);
114
- });
115
-
116
- test("redacts secret even inside ANSI-styled line", () => {
117
- const input = "\x1b[33mAPI_KEY=\x1b[31msecretvalue123\x1b[0m";
118
- const out = scrubSecrets(input);
119
- assert(!out.includes("secretvalue123"), `got: ${out}`);
120
- assert(out.includes(_REDACTED));
121
- });
122
-
123
- test("ANSI bold output without secrets passes through", () => {
124
- const input = "\x1b[1mBuilding project...\x1b[0m\nCompiled successfully.";
125
- assert.strictEqual(scrubSecrets(input), input);
126
- });
127
-
128
- // --- 4. Edge cases ---
129
-
130
- test("handles empty string", () => {
131
- assert.strictEqual(scrubSecrets(""), "");
132
- });
133
-
134
- test("handles null/undefined", () => {
135
- assert.strictEqual(scrubSecrets(null), null);
136
- assert.strictEqual(scrubSecrets(undefined), undefined);
137
- });
138
-
139
- test("handles very long line (>1000 chars) without secrets", () => {
140
- const longLine = "x".repeat(2000);
141
- assert.strictEqual(scrubSecrets(longLine), longLine);
142
- });
143
-
144
- test("handles very long line with embedded secret", () => {
145
- const prefix = "a".repeat(500);
146
- const suffix = "b".repeat(500);
147
- const key = "sk-" + "c".repeat(48);
148
- const input = `${prefix} ${key} ${suffix}`;
149
- const out = scrubSecrets(input);
150
- assert(!out.includes(key), `key should be redacted`);
151
- });
152
-
153
- test("handles rapid multi-line output (100 lines)", () => {
154
- const lines = Array.from({ length: 100 }, (_, i) => `line ${i}: output data here`);
155
- const input = lines.join("\n");
156
- assert.strictEqual(scrubSecrets(input), input);
157
- });
158
-
159
- test("handles multi-line with mixed secrets and normal output", () => {
160
- const input = [
161
- "Starting deploy...",
162
- "AWS_SECRET_ACCESS_KEY=AKIAIOSFODNN7EXAMPLE",
163
- "Uploading artifacts...",
164
- "Bearer eyJhbGciOiJIUzI1NiJ9.xxxxxxxxxxxx",
165
- "Deploy complete!",
166
- ].join("\n");
167
- const out = scrubSecrets(input);
168
- assert(out.includes("Starting deploy..."));
169
- assert(out.includes("Uploading artifacts..."));
170
- assert(out.includes("Deploy complete!"));
171
- assert(!out.includes("AKIAIOSFODNN7EXAMPLE"));
172
- assert(!out.includes("eyJhbGci"));
173
- });
174
-
175
- // --- 5. scrubScrollback (string output for WebSocket text frames) ---
176
-
177
- test("scrubScrollback returns string, not Buffer", () => {
178
- const buf = Buffer.from("SOME_SECRET_KEY=abc123\nnormal line");
179
- const out = scrubScrollback(buf);
180
- assert.strictEqual(typeof out, "string", "should return a string for text WebSocket frames");
181
- assert(!out.includes("abc123"));
182
- });
183
-
184
- test("scrubScrollback handles empty buffer", () => {
185
- const buf = Buffer.alloc(0);
186
- const out = scrubScrollback(buf);
187
- assert.strictEqual(typeof out, "string");
188
- assert.strictEqual(out, "");
189
- });
190
-
191
- test("scrubScrollback handles null", () => {
192
- assert.strictEqual(scrubScrollback(null), "");
193
- });
194
-
195
- test("scrubScrollback preserves non-secret content", () => {
196
- const content = "Hello world\nBuild succeeded\n42 tests passed";
197
- const buf = Buffer.from(content);
198
- const out = scrubScrollback(buf);
199
- assert.strictEqual(typeof out, "string");
200
- assert.strictEqual(out, content);
201
- });
202
-
203
- // --- 6. Integration path verification ---
204
- // These tests verify that server/index.js imports from the same module
205
- // we're testing, ensuring no copy-paste drift.
206
-
207
- test("server/index.js imports scrubSecrets from scrub-secrets.js", () => {
208
- const serverSource = require("fs").readFileSync(
209
- require("path").join(__dirname, "..", "index.js"), "utf-8"
210
- );
211
- assert(
212
- serverSource.includes('require("./scrub-secrets")') ||
213
- serverSource.includes("require('./scrub-secrets')"),
214
- "server/index.js must import from ./scrub-secrets"
215
- );
216
- });
217
-
218
- test("server/index.js uses scrubSecrets in PTY data handler", () => {
219
- const serverSource = require("fs").readFileSync(
220
- require("path").join(__dirname, "..", "index.js"), "utf-8"
221
- );
222
- assert(serverSource.includes("scrubSecrets(data)"), "live PTY path must call scrubSecrets");
223
- });
224
-
225
- test("server/index.js uses scrubScrollback in replay handler", () => {
226
- const serverSource = require("fs").readFileSync(
227
- require("path").join(__dirname, "..", "index.js"), "utf-8"
228
- );
229
- assert(serverSource.includes("scrubScrollback(session.scrollback)"), "replay path must call scrubScrollback");
230
- });
231
-
232
- // --- Summary ---
233
-
234
- console.log(`\nResults: ${passed} passed, ${failed} failed, ${passed + failed} total\n`);
235
- process.exit(failed > 0 ? 1 : 0);