arisa 5.2.21 → 5.2.23

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "arisa",
3
- "version": "5.2.21",
3
+ "version": "5.2.23",
4
4
  "description": "Telegram + Pi Agent modular assistant",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -33,6 +33,15 @@ export function getPiAuthIssue(error) {
33
33
  return null;
34
34
  }
35
35
 
36
+ export function buildPiRequestFailureMessage(error) {
37
+ const message = getErrorMessage(error);
38
+ const limited = /usage limit|quota|rate[ _-]?limit|too many requests|insufficient[_ ]credits|credits? (?:exhausted|depleted)/i.test(message);
39
+ const title = limited
40
+ ? "The provider's usage quota or request limit was reached. Retry when capacity is available; signing in again does not restore quota."
41
+ : "Pi request could not be completed. This does not establish an authentication failure.";
42
+ return `${title}\nDetails: ${message}`;
43
+ }
44
+
36
45
  export async function getPiAuthStatus(config, chatId = null) {
37
46
  const runtime = await createPiRuntime({
38
47
  provider: config.pi.provider,
@@ -149,7 +149,9 @@ async function installPackageDependencies(toolDir) {
149
149
  }
150
150
  const dependencyCount = Object.keys(packageJson.dependencies || {}).length + Object.keys(packageJson.optionalDependencies || {}).length;
151
151
  if (!dependencyCount) return { installed: false };
152
- if (await exists(path.join(toolDir, "package-lock.json"))) {
152
+ if (await exists(path.join(toolDir, "pnpm-lock.yaml"))) {
153
+ await runCommand("pnpm", ["install", "--frozen-lockfile", "--prod"], { cwd: toolDir });
154
+ } else if (await exists(path.join(toolDir, "package-lock.json"))) {
153
155
  await runCommand("npm", ["ci", "--omit=dev"], { cwd: toolDir });
154
156
  } else {
155
157
  await runCommand("npm", ["install", "--omit=dev"], { cwd: toolDir });
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 1,
3
3
  "repository": "https://github.com/clasen/Arisa.git",
4
- "commit": "2256f9e19f26f23826b9a9062d3a9643b511bb3c",
4
+ "commit": "773dc5b7dd81e628ec63632bd9d7c32e4cb468fd",
5
5
  "tools": {
6
6
  "audio-extractor": {
7
7
  "version": null,
@@ -13,42 +13,50 @@
13
13
  }
14
14
  },
15
15
  "browser-session-bridge": {
16
- "version": "0.2.5",
16
+ "version": "0.2.6",
17
17
  "files": {
18
- "README.md": "79cb347ead539309429eca19ba6e9c472e4790ec1ab39a20bba71a4149f61f15",
19
- "bridge-server.js": "03722a2657ba74bf34de9cae683b8f23242657da87c05771dd413a38fb446eca",
18
+ "README.md": "f650843cf4a9b3cb41c3a9a0fafa9c90af5dc607eb142dbc0a215203862b408d",
19
+ "bridge-server.js": "e5181ef56555a018ba729272409661f4da1b3ac29d2ac5d7e6a5d8e25e93a861",
20
20
  "chrome-web-store.js": "4b588d1288dee754c5d8afd609b8c1f67f73d23de7dda247b7a2ff8b68f7ef60",
21
21
  "config.js": "45c8e9e9a159b9125d6ea3458c616926c2ba2b5327fdfd750aa05316d3ce5af8",
22
+ "extension/background-workflow.js": "5ea3afccdfcbfaf9ee41f3baa910f75177a863aeed96d924c9c253e4d23720ea",
23
+ "extension/background.js": "7a7e7217a9e952739cb5f6cf06ad1002424cb047a045fb29d6642b2b30449bee",
24
+ "extension/bridge-client.js": "f4b71ccd816734a51dd49ca9240cc2d436c6f2584f40de1cd59c7f4d742426bc",
22
25
  "extension/icons/icon-128.png": "a55af49bb570bd5471fc0a2e15a6bcaf3f76a2c5e7439f16bb1829682b343380",
23
26
  "extension/icons/icon-16.png": "a4aedb49e2227222a68dc56455812a9e17938fd197b1f30268f02d6c9f7bf95e",
24
27
  "extension/icons/icon-32.png": "2dfc9e6e7ec6b4e9feb9c1326d65116cda8e53306c65c84074c818c9e7f6a2a2",
25
28
  "extension/icons/icon-48.png": "2f316f1f2fbc72227e7e1f14cf1ff1553a12fc9aab139799f1ee2970c0303deb",
26
- "extension/manifest.json": "fbf3dd9fcb119eaaa2a0e43720096c19eb2d3ceca45ea5a893a00bfaa5b44452",
27
- "extension/onboarding-state.js": "79f5601ac66686b0d371bbfa8c9e38e12fd23213f3ad309f8c9c6d55c1f856cb",
29
+ "extension/manifest.json": "abbc939749585fefffac51e6c2d6b0c0511771d43008a1ce9048a89e02879fa7",
30
+ "extension/onboarding-state.js": "c4c716ac6bb3c942f6ac8fb029693de96db7d5c8e84c886bd026106fb74206c3",
28
31
  "extension/popup.css": "8044a298f91790d38c8e84823d10951baffcf823bee5c330f3f297db125a5726",
29
- "extension/popup.html": "2f8590991f9941b50ad61d0a7563a0a073eae5a5e980251fe16e19fe86c7a94f",
30
- "extension/popup.js": "cf4bb363403381590b872991642f38b98c0964fdd9c3df00b6ec3b40b90c9be9",
32
+ "extension/popup.html": "8b24f47f092943f954f3a403af209809b5d935b20de6c291ce3d576bd23f8b6c",
33
+ "extension/popup.js": "d762108f2071424f702ab845254d86fc48c0dc0461143cb1a384820147a08c4e",
31
34
  "extension/session-send-state.js": "ec574454825679d8d3eb1fc5692d9a9fcbf133f3d9ac3621648ad09e76ff4abc",
32
35
  "extension/site-permissions.js": "83f9aa4d0295318fc730bd561c2db9d1aacf36152319090e99d12f62deea11ad",
33
- "index.js": "c01a6b825da4587a550ecec560a558cc6736200e73b6831f5919000c7b81679d",
36
+ "import-receipts.js": "3f8fb5f3604a73f7b56d7bc9d32cf7b638e7d86065952155fbb4ffb70134d478",
37
+ "index.js": "351af3e57a90096833b64c33169118ce4b75b81e8aa572090a45422a214646b6",
34
38
  "lightpanda-session.js": "57c713dafac531617d7285122b2098c1e0b2f334e43144632f2b1fc79405f789",
35
- "package-lock.json": "b97f5a333df234fecde788d0f90b620d39e02fee6079c1b4347baaabd8010b1b",
36
- "package.json": "5c9edd6155b6b4868cc87d474e808dfc23567bc312e0d1e0230d8f9f0a1cc081",
39
+ "package.json": "c6fba5540eef5953ca1e836388c82d006be803d69ebf3d246b8ab9adbea85ad1",
40
+ "pnpm-lock.yaml": "7af346d8eb7cfaa293cdb329fdc0b3597da5aa8f22b5001e3d8e8c464ab2e9a7",
37
41
  "session-browser.js": "7af716a1c0d6793b82ebbaaf64ec3e8f73dfc52b3c9aceef2179d2d859e32a9c",
38
42
  "session-redaction.js": "c2393023c1d3b7634432606ad82a96d79c1f093d9b0a6440d955d0ae419c8393",
39
43
  "session-selection.js": "da5a4f55ccc8ba22791840943288e7aecacd730425c03f05c2bc10fd6a14341f",
40
44
  "session-store.js": "ee85e5b9696e542bb535a996743ddbbd2093fc66e18873a2f1c7a27707a83994",
41
- "test/device-enrollment.test.js": "a4246f4cb19e959ef4b8a83f60664887b624526b0e5646e5ec1fdb93750082b0",
45
+ "test/background-recovery.test.js": "77d28d9a1aa0bcb91b45b5a1e9be0c030ee6f831cc4b1ad698ccb23dfdbce3be",
46
+ "test/background-workflow.test.js": "9f8bb38d713b01c1b88af41c3ba082f3b0e438b79116c6cd9bd4413cc4c9be16",
47
+ "test/device-enrollment.test.js": "f8c5cb944b4416537702173656239a9d2f535fef67c14fefa9c75e6118ab1776",
48
+ "test/import-receipts.test.js": "29232c3e4a84f2548cfedc689e26010769cdaad2695437cbdd278fbbc1df360a",
42
49
  "test/lightpanda-session.test.js": "1fa6c1693a5008c173ec6c5c97effe001b68c535c7a3d4530500c0c4b691c532",
43
50
  "test/onboarding-state.test.js": "889f7746bbdb51320e68447560459d2a5d812e4b135de08a7b6e94ef5a142250",
51
+ "test/popup-lifecycle.test.js": "2326088ad4410849867323ad07f7679906664c67f8fb062cb47c4d3dff05021c",
44
52
  "test/session-redaction.test.js": "e45b5cbd19eea7996d57141b5185c3a6e4c79989e4cb7253272fc66cd2cbd719",
45
53
  "test/session-refresh.test.js": "ffa688433185c08696761f189df244ed9ac212731b012a3eb9a91da05b43dd56",
46
54
  "test/session-selection.test.js": "ee1a15d42b156403d46d789a3ecf0a9c8f0019a00ab908276297d031243a89f5",
47
55
  "test/session-send-state.test.js": "c8ccbdf0863c5de85f784e7e32436591fe2db16a2ef4c554f671c6352736d5b6",
48
56
  "test/site-permissions.test.js": "27c297475c38ee1cbe7c7df210c767c157e03ecd379cde99a1b502268bda56ab",
49
- "tool.manifest.json": "820f6060f077b95c286168f175e5521bc67b043425cae69c5be40834d5016454",
50
- "web-store/listing.md": "3e6d7aab1f60142984c3f8c18d92bf23646dc901644c6b71bdb979802bcf320c",
51
- "web-store/privacy-policy.md": "eaaaa6066b36ef56896321d62b6ac4596d62104a0591d4edceb8c6040c47fd5c"
57
+ "tool.manifest.json": "c62e085045cd8b922b8aa97b676f5ff263aac3a6861500bd17fb3a237a6e425e",
58
+ "web-store/listing.md": "ee3ac9efa32cfe7ca67f995cab057d1db40f40eb237f014e4f699885a022a318",
59
+ "web-store/privacy-policy.md": "245cd64e0e7687b0c66ebb4cd0e0ee55856a1713c51d4831f22a332db242b168"
52
60
  },
53
61
  "toolDependencies": {
54
62
  "lightpanda-browser": "^0.11.5"
@@ -122,10 +130,10 @@
122
130
  "config.js": "bab728665650b986c0eaef8e1be1c259ec42b1f83df4217c9780f5468d3c7666",
123
131
  "index.js": "a544e5dbac9cb92e996c2b07690a18481a20b8b10c13c397412581ec9ac6bb04",
124
132
  "outcome-contract.js": "5685bd8fbe4fcb608ee43a0148201206283013c1a60855c974a928c1d312480a",
125
- "package-lock.json": "a2e847954e9d6682b8fd435c46d7ecbf38568147fc3d336e10f6d806f6be620a",
126
- "package.json": "19c61d77ec90fce64d1a3baa1acc5b29af23f7c0574964504bc69b19e38aa34f",
133
+ "package.json": "593c9afd3d151f8107c611724e7218ed3970eb7643273994ce6006d2af20abcf",
127
134
  "panda-results.js": "24a31263b4a03e2c2719d9c54be7b5c022b17fb7dcb249eed33b93882518145b",
128
135
  "panda-session.js": "69878ff01bfbf26afe4db0e2e6489b211e3aca100ba5353befefaea009636bb5",
136
+ "pnpm-lock.yaml": "7af346d8eb7cfaa293cdb329fdc0b3597da5aa8f22b5001e3d8e8c464ab2e9a7",
129
137
  "reference-match.js": "5ceb71bfe57763258d89c60936bb7e5b3166b6aa7127f9d2f8da49f01298f46b",
130
138
  "test/outcome-contract.test.js": "dda85d1f56880b30d7fad3e8a27aacbb0808d91fc0addac6e0c38f9f0c4849bf",
131
139
  "test/panda-results.test.js": "b918d6341251822292fe3e375ab66694a0f71f3c7a24e91be263ee36e5f3e92c",
@@ -187,7 +195,7 @@
187
195
  "BENCHMARK.md": "9c732c65f6d8bceb4e73c1a03d298d5140d919f50269f982bd0ec6c1b21fa8b6",
188
196
  "COMPATIBILITY-MIGRATION.md": "230e12e8ddac837b7772519693e530c43d2158b69c671036bf246fb3a9e35561",
189
197
  "LIMITS.md": "6c0ec9624c76de8ddf09b82a4da1dba1b89a48a1f0e71a083ffbcf7a3d5401fb",
190
- "README.md": "c79a9a24d2fe804a7ce3e160e8308b1ac0039461cbfd012347aaa61f841f5fed",
198
+ "README.md": "6cd4b4b6443d8b568537f9481de23c60e037deb138215f5b5393cc1f49020c1f",
191
199
  "action-policy.js": "66c6ad39f65378ccf8ed54177f5a3665e23ef82818c30fb05d28cd58cba9b2ab",
192
200
  "authenticated-profile.js": "2bbb96c4d25a108c44256514c1c9b4e61e1f125e470ab35c5e02515216cd1b33",
193
201
  "benchmark-latest.json": "c5524e6d42479578579fd7db0e50857ff0831ca219e611b821ad1e0a4fade303",
@@ -202,8 +210,8 @@
202
210
  "limit-suite-policy.js": "61440ee2e7f557ab2f3c74f8ef4ec2ec85fd2fb7fc53a4121e1f0d066670ea17",
203
211
  "mcp-session.js": "cd4a1e6d3c9d80c399fda8f313d69e38fa22a9b24789d7919832a56df7dc90c2",
204
212
  "output-bounds.js": "08ab730d0d8722c5e0e761911a509653dd23d8eb4d8c2abf2d9c5e8432870bdd",
205
- "package-lock.json": "815dfdb89ce94b543e520955480e322438abd2b71fad4c4ddd699838b8622b65",
206
- "package.json": "934ae0f55d1baea305c28f12c0c9faccdd864244d899663c96f9083d96442841",
213
+ "package.json": "54bf5020ac3ec8a682491dcfcf877b50dc239b6aee72afad039ce473fbc794c2",
214
+ "pnpm-lock.yaml": "17c814b167307942d3609c7b9d916ceddb85839573ab39baa114e30edb132a1a",
207
215
  "process-metrics.js": "e39d2980abb581ca0b917d0d33a1d71922ce49b679a7e96cf16a6a66b81d879a",
208
216
  "recipe-store.js": "358ac391ae86e0bfb82fc5b55bd08cfe3fd4fa56ac12a5ad4ad5abfa43c9a6c6",
209
217
  "scripts/benchmark.js": "f1bb939984460d84118317f9b4f811e0df80f590b0a441cd62a56eb97bb69b37",
@@ -307,8 +315,8 @@
307
315
  "network-security.js": "60a020d4e3c7a4784a5a4e7b7246ee34e63ac73ee2316e09a09d10668dc8a7bc",
308
316
  "oauth-device.js": "698aa9255bae47190c63527db7680aaab4f06392a108a0ff6444e32d31b7b89f",
309
317
  "oauth-watch-plan.js": "b1bbed52eeb0e2e50dd52d733897ebd70573fea3ff5f023b3c63347f92633d3e",
310
- "package-lock.json": "fd7b6bf4545de4056908bd656a4a4e21b9f84acc7b06d8c1d581a8c84c295e90",
311
- "package.json": "85ac369f959a40da258d268a3ab1d523abba7c225d5c44a903ddae0bcc512d1b",
318
+ "package.json": "15e02c7df79f72215e3b13af3aa8d6eba9ddd281509dc6bfeffc9ceb43bcba92",
319
+ "pnpm-lock.yaml": "fa60c4eced54fabd33553bd7e0e8e313e9749fea80381e333bfeb3c028aef879",
312
320
  "state-store.js": "a8fb63096b8efdec8de1cb386eff371f1552f3fd71d96f25036e1c724285e0af",
313
321
  "test/mcp-client.test.js": "26a7fe3ffa33ad454eced32c7fab12975ac5c0914041dea68cb7e5467e753a10",
314
322
  "tool-routing.js": "b3405c9879443cdc257a1e97d4968859a8654e62ff7cfb90ec3a7a992a62e311",
@@ -620,19 +628,20 @@
620
628
  }
621
629
  },
622
630
  "whatsapp-web": {
623
- "version": "1.3.4",
631
+ "version": "1.3.5",
624
632
  "files": {
625
633
  "apply-whatsapp-web-compat.js": "8216698bf6d435d516ef8d10e47ea1fa636e81794ec66cde7e52a029f1bd89a0",
626
634
  "browser-cache.js": "f71c20f9ab5f04d9d46c5e2551b348d47baba89a10a07173070298098a6d5551",
627
635
  "config.js": "b0d4bed04dc87828ff61e1c9050a67ebf09a39cdc3ca52ce23be4c31476dda08",
628
636
  "incoming-wake-gate.js": "13b773f47e4e92910ac8dcb9ebb31a0cc7f244791105f45cd61c387a7f2f03cc",
629
- "index.js": "461cecd656ba52245de6b76b009b6d335c2ded24eb7b1f163f7c7b7e50ab02f3",
637
+ "index.js": "5a1b6c36e7b02bc678aa0ed7194c669672945644c20a8e7452c9c206c2900ddb",
630
638
  "message-burst.js": "06b2348ea409dffff74c9ae5a6d882f3d279f4adc39206e019f73df424dc4b3b",
631
- "package.json": "5e0ddf226fe8658fd78d2e1411f43ff816cc09726a827e0cdb2c151a2a8e8189",
639
+ "package.json": "f9c45c408616af49097746d055d1b5005ab955f642816daee7896887fc514130",
632
640
  "test/browser-cache.test.mjs": "636b91c16c9d4a67f38d625293fe371eb3d2e91818d131cacdec15e5bb255a69",
633
641
  "test/incoming-wake-gate.test.mjs": "f84f4f2e6c8d8ff2b7b63f9a266b15d7234bb017400826eac4f96a675cd7e25d",
634
642
  "test/message-burst.test.mjs": "d994f46054c7b464d47d714e6e364df5ba96206ce0b7069fccbb555fd61ed14f",
635
- "tool.manifest.json": "9e40152ece0ba3ae7a1ad2c462291a5336c2d72492b4293f8f574923d2a762d6"
643
+ "test/task-ipc.test.mjs": "8f1221d05d1f97f204ea244fb77d088e538f93aa22ea45272d09bef56c762172",
644
+ "tool.manifest.json": "5106aae4fc5ae17b7babd7658437880170842e037dd6bbebe4a72729d203b663"
636
645
  }
637
646
  },
638
647
  "whispermix-transcribe": {
@@ -1,6 +1,7 @@
1
1
  import {
2
2
  buildPiAuthRecoveryBlockedMessage,
3
3
  buildPiAuthTelegramMessage,
4
+ buildPiRequestFailureMessage,
4
5
  getErrorMessage,
5
6
  getPiAuthIssue,
6
7
  getPiAuthStatus
@@ -37,12 +38,16 @@ export function createTelegramAuthController({
37
38
  }
38
39
 
39
40
  function rememberValidationFailure(error) {
40
- const detected = rememberIssue(error) || {
41
- kind: "validation-failed",
42
- message: getErrorMessage(error)
43
- };
44
- issue = detected;
45
- return detected;
41
+ // Quota, network and server failures must never latch the Telegram auth gate.
42
+ issue = getPiAuthIssue(error);
43
+ return issue;
44
+ }
45
+
46
+ async function buildValidationFailureMessage(chatId, error) {
47
+ const detected = rememberValidationFailure(error);
48
+ return detected
49
+ ? buildPiAuthTelegramMessage({ config, chatId, issue: detected })
50
+ : buildPiRequestFailureMessage(error);
46
51
  }
47
52
 
48
53
  async function notifyIssueIfNeeded(chatId, error) {
@@ -62,15 +67,14 @@ export function createTelegramAuthController({
62
67
  async function finishRenewal(chatId, renewal) {
63
68
  try {
64
69
  await renewal.promise;
65
- await agentManager.validateAgent();
66
- agentManager.clearSessionCache(chatId);
67
70
  issue = null;
71
+ agentManager.clearSessionCache(chatId);
72
+ await agentManager.validateAgent();
68
73
  logger?.log("telegram", `Pi auth renewal completed for chat ${chatId}`);
69
74
  await api.sendMessage(chatId, await buildPiAuthTelegramMessage({ config, chatId, verified: true }));
70
75
  } catch (error) {
71
- const detected = rememberValidationFailure(error);
72
- logger?.error("telegram", `Pi auth renewal failed for chat ${chatId}: ${getErrorMessage(error)}`);
73
- await api.sendMessage(chatId, await buildPiAuthTelegramMessage({ config, chatId, issue: detected })).catch((notifyError) => {
76
+ logger?.error("telegram", `Pi auth renewal or validation failed for chat ${chatId}: ${getErrorMessage(error)}`);
77
+ await api.sendMessage(chatId, await buildValidationFailureMessage(chatId, error)).catch((notifyError) => {
74
78
  logger?.error("telegram", `auth renewal failure notice failed for chat ${chatId}: ${getErrorMessage(notifyError)}`);
75
79
  });
76
80
  } finally {
@@ -141,7 +145,9 @@ export function createTelegramAuthController({
141
145
  if (!authorization.ok) return;
142
146
 
143
147
  const status = await getPiAuthStatus(config, ctx.chat.id);
144
- if (status.hasApiKey || !status.supportsOAuth) {
148
+ const canValidateStoredAuth = status.hasStoredAuth && !issue && !renewals.has(chatKey(ctx.chat.id));
149
+ // A stored OAuth login may still be valid even when the provider has no quota.
150
+ if (status.hasApiKey || !status.supportsOAuth || canValidateStoredAuth) {
145
151
  await withTyping(ctx, async () => {
146
152
  try {
147
153
  await agentManager.validateAgent();
@@ -149,8 +155,7 @@ export function createTelegramAuthController({
149
155
  issue = null;
150
156
  await ctx.reply(await buildPiAuthTelegramMessage({ config, chatId: ctx.chat.id, verified: true }));
151
157
  } catch (error) {
152
- const detected = rememberValidationFailure(error);
153
- await ctx.reply(await buildPiAuthTelegramMessage({ config, chatId: ctx.chat.id, issue: detected }));
158
+ await ctx.reply(await buildValidationFailureMessage(ctx.chat.id, error));
154
159
  }
155
160
  });
156
161
  return;
@@ -162,8 +167,7 @@ export function createTelegramAuthController({
162
167
  ? "Starting Pi login from Telegram..."
163
168
  : "Pi login is already in progress. Paste the redirect URL or code here when you have it.");
164
169
  } catch (error) {
165
- const detected = rememberValidationFailure(error);
166
- await ctx.reply(await buildPiAuthTelegramMessage({ config, chatId: ctx.chat.id, issue: detected }));
170
+ await ctx.reply(await buildValidationFailureMessage(ctx.chat.id, error));
167
171
  }
168
172
  }
169
173
 
@@ -1,7 +1,7 @@
1
1
  import assert from "node:assert/strict";
2
2
  import crypto from "node:crypto";
3
3
  import { execFileSync } from "node:child_process";
4
- import { cp, mkdir, mkdtemp, readFile, readdir, rm, symlink, writeFile } from "node:fs/promises";
4
+ import { chmod, cp, mkdir, mkdtemp, readFile, readdir, rm, symlink, writeFile } from "node:fs/promises";
5
5
  import os from "node:os";
6
6
  import path from "node:path";
7
7
  import { fileURLToPath } from "node:url";
@@ -136,6 +136,44 @@ test("installs a verified staged tree without overwriting an existing tool", asy
136
136
  );
137
137
  });
138
138
 
139
+ for (const exitCode of [0, 1]) {
140
+ test(`uses the pnpm lock without falling back to npm when pnpm exits ${exitCode}`, async (t) => {
141
+ const { root, source, files } = await fixture(t);
142
+ for (const [name, content] of Object.entries({
143
+ "package.json": JSON.stringify({ dependencies: { example: "1.0.0" } }),
144
+ "pnpm-lock.yaml": "lockfileVersion: '9.0'\n"
145
+ })) {
146
+ await writeFile(path.join(source, name), content);
147
+ files[name] = await digest(path.join(source, name));
148
+ }
149
+ const bin = path.join(root, "bin");
150
+ const callsFile = path.join(root, "calls.jsonl");
151
+ await mkdir(bin);
152
+ for (const manager of ["pnpm", "npm"]) {
153
+ const executable = path.join(bin, manager);
154
+ await writeFile(executable, `#!${process.execPath}\nrequire('node:fs').appendFileSync(${JSON.stringify(callsFile)}, JSON.stringify([${JSON.stringify(manager)}, ...process.argv.slice(2)]) + '\\n');\nprocess.exit(${manager === "pnpm" ? exitCode : 0});\n`);
155
+ await chmod(executable, 0o755);
156
+ }
157
+ const previousPath = process.env.PATH;
158
+ process.env.PATH = `${bin}${path.delimiter}${previousPath}`;
159
+ t.after(() => { process.env.PATH = previousPath; });
160
+ const install = () => installLockedOfficialTool({
161
+ toolName: "master-slave",
162
+ lock: lock(files),
163
+ destination: path.join(root, "installed", "master-slave"),
164
+ checkout: async ({ checkoutDir }) => {
165
+ await mkdir(path.join(checkoutDir, "tools"), { recursive: true });
166
+ await cp(source, path.join(checkoutDir, "tools", "master-slave"), { recursive: true });
167
+ },
168
+ validate: async () => {}
169
+ });
170
+ if (exitCode) await assert.rejects(install, /pnpm failed/);
171
+ else await install();
172
+ const calls = (await readFile(callsFile, "utf8")).trim().split("\n").map(JSON.parse);
173
+ assert.deepEqual(calls, [["pnpm", "install", "--frozen-lockfile", "--prod"]]);
174
+ });
175
+ }
176
+
139
177
  test("loads the bundled lock before selecting the canonical tool destination", async (t) => {
140
178
  const { root, files } = await fixture(t);
141
179
  const lockFile = path.join(root, "official-tools.lock.json");
@@ -0,0 +1,124 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import { ModelRuntime } from "@earendil-works/pi-coding-agent";
4
+ import { createTelegramAuthController } from "../src/transport/telegram/telegram-auth-controller.js";
5
+
6
+ function setup(t, { stored = true, apiKey = "", failure = null, loginFailure = null } = {}) {
7
+ const messages = [];
8
+ const calls = { login: 0, validate: 0, clear: 0 };
9
+ t.mock.method(ModelRuntime, "create", async () => ({
10
+ setRuntimeApiKey: async () => {},
11
+ getProviderAuthStatus: () => ({ configured: stored, source: "stored" }),
12
+ getProvider: () => ({ auth: { oauth: {} } }),
13
+ async login() {
14
+ calls.login++;
15
+ if (loginFailure) throw loginFailure;
16
+ return { type: "oauth", access: "test-only" };
17
+ }
18
+ }));
19
+ const controller = createTelegramAuthController({
20
+ config: { pi: { provider: "openai-codex", model: "test-model", apiKey } },
21
+ api: { sendMessage: async (_chatId, text) => messages.push(text) },
22
+ agentManager: {
23
+ async validateAgent() {
24
+ calls.validate++;
25
+ if (failure) throw failure;
26
+ },
27
+ clearSessionCache() { calls.clear++; }
28
+ }
29
+ });
30
+ const command = () => controller.handleCommand({
31
+ chat: { id: 123 }, reply: async (text) => messages.push(text)
32
+ }, { authorize: async () => ({ ok: true }), withTyping: async (_ctx, fn) => fn() });
33
+ return { controller, messages, calls, command };
34
+ }
35
+
36
+ for (const apiKey of ["", "test-key"]) {
37
+ for (const message of [
38
+ "Codex error: The usage limit has been reached",
39
+ "429 Too many requests",
40
+ "insufficient_quota",
41
+ "request timed out",
42
+ "503 service unavailable"
43
+ ]) {
44
+ test(`validation preserves usable auth after ${message} (${apiKey ? "API key" : "OAuth"})`, async (t) => {
45
+ const { controller, calls, command, messages } = setup(t, { apiKey, failure: new Error(message) });
46
+ await command();
47
+ assert.equal(controller.getIssue(), null);
48
+ assert.equal(calls.login, 0);
49
+ assert.equal(calls.clear, 0);
50
+ assert.match(messages[0], new RegExp(message));
51
+ assert.doesNotMatch(messages[0], /Send \/auth|Run \/auth|Update the key|not ready/);
52
+ await command();
53
+ assert.equal(calls.validate, 2);
54
+ assert.equal(calls.login, 0);
55
+ });
56
+ }
57
+ }
58
+
59
+ test("/auth validates stored OAuth credentials instead of starting a device login", async (t) => {
60
+ const { controller, calls, command, messages } = setup(t);
61
+ await command();
62
+ assert.equal(calls.validate, 1);
63
+ assert.equal(calls.login, 0);
64
+ assert.equal(controller.getIssue(), null);
65
+ assert.match(messages[0], /authentication is working/);
66
+ });
67
+
68
+ test("real token invalidation remains blocking and permits OAuth recovery", async (t) => {
69
+ const { controller, command, calls, messages } = setup(t, { failure: new Error("auth token revoked") });
70
+ await command();
71
+ assert.equal(controller.getIssue()?.kind, "invalidated-token");
72
+ assert.match(messages[0], /Run \/auth/);
73
+ await command();
74
+ await new Promise((resolve) => setImmediate(resolve));
75
+ assert.equal(calls.login, 1);
76
+ assert.equal(controller.getIssue()?.kind, "invalidated-token");
77
+ });
78
+
79
+ test("successful OAuth renewal followed by quota failure clears a stale auth block", async (t) => {
80
+ const { controller, command, calls, messages } = setup(t, {
81
+ failure: new Error("Codex error: The usage limit has been reached")
82
+ });
83
+ controller.rememberIssue(new Error("auth token expired"));
84
+ await command();
85
+ await new Promise((resolve) => setImmediate(resolve));
86
+ assert.equal(calls.login, 1);
87
+ assert.equal(calls.clear, 1);
88
+ assert.equal(controller.getIssue(), null);
89
+ assert.equal(controller.hasActiveRenewal(123), false);
90
+ assert.match(messages.join("\n"), /signing in again does not restore quota/);
91
+ await command();
92
+ assert.equal(calls.login, 1);
93
+ assert.equal(calls.validate, 2);
94
+ });
95
+
96
+ test("missing credentials still initiate login", async (t) => {
97
+ const { controller, command, calls } = setup(t, { stored: false });
98
+ await command();
99
+ await new Promise((resolve) => setImmediate(resolve));
100
+ assert.equal(calls.login, 1);
101
+ assert.equal(calls.validate, 1);
102
+ assert.equal(controller.getIssue(), null);
103
+ });
104
+
105
+ test("transient OAuth login failure does not create an authentication block", async (t) => {
106
+ const { controller, command, calls, messages } = setup(t, {
107
+ stored: false, loginFailure: new Error("request timed out")
108
+ });
109
+ await command();
110
+ await new Promise((resolve) => setImmediate(resolve));
111
+ assert.equal(calls.validate, 0);
112
+ assert.equal(controller.getIssue(), null);
113
+ assert.equal(controller.hasActiveRenewal(123), false);
114
+ assert.match(messages.join("\n"), /does not establish an authentication failure/);
115
+ });
116
+
117
+ test("ordinary prompt quota errors never latch the auth issue", async (t) => {
118
+ const { controller, messages } = setup(t);
119
+ assert.equal(await controller.notifyIssueIfNeeded(123, new Error("Codex error: The usage limit has been reached")), false);
120
+ assert.equal(controller.getIssue(), null);
121
+ assert.equal(messages.length, 0);
122
+ assert.equal(await controller.notifyIssueIfNeeded(123, new Error("No auth found")), true);
123
+ assert.equal(controller.getIssue()?.kind, "missing-auth");
124
+ });