pi-oracle 0.7.21 → 0.7.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/CHANGELOG.md +40 -0
- package/README.md +49 -385
- package/docs/ORACLE_DESIGN.md +1 -1
- package/docs/development.md +99 -0
- package/docs/platform-smoke.md +18 -7
- package/docs/reference.md +338 -0
- package/extensions/oracle/index.ts +1 -0
- package/extensions/oracle/lib/poller.ts +3 -8
- package/extensions/oracle/lib/queue.ts +2 -4
- package/extensions/oracle/shared/browser-profile-helpers.d.mts +1 -0
- package/extensions/oracle/shared/browser-profile-helpers.mjs +32 -0
- package/extensions/oracle/worker/auth-bootstrap.mjs +35 -6
- package/extensions/oracle/worker/chatgpt-ui-helpers.d.mts +1 -0
- package/extensions/oracle/worker/chatgpt-ui-helpers.mjs +18 -13
- package/extensions/oracle/worker/chromium-cookie-source.mjs +50 -6
- package/extensions/oracle/worker/linux-chromium-cookie-source.d.mts +6 -0
- package/extensions/oracle/worker/linux-chromium-cookie-source.mjs +98 -0
- package/extensions/oracle/worker/run-job.mjs +34 -34
- package/package.json +12 -11
- package/platform-smoke.config.mjs +1 -22
- package/scripts/oracle-auth.test.mjs +35 -0
- package/scripts/oracle-chatgpt-preset-proof.mjs +50 -21
- package/scripts/oracle-cookie-import.test.mjs +204 -0
- package/scripts/oracle-model.test.mjs +24 -17
- package/scripts/oracle-real-smoke.mjs +30 -9
- package/scripts/platform-smoke/Dockerfile.ubuntu +3 -2
- package/scripts/platform-smoke/crabbox-runner.mjs +18 -6
- package/scripts/platform-smoke/doctor.mjs +58 -41
- package/scripts/platform-smoke/invariants.mjs +254 -26
- package/scripts/platform-smoke/platform-build-windows.ps1 +21 -17
- package/scripts/platform-smoke/targets.mjs +59 -30
- package/scripts/platform-smoke.mjs +47 -14
|
@@ -26,7 +26,7 @@ import {
|
|
|
26
26
|
CHATGPT_COMPOSER_LABELS,
|
|
27
27
|
deriveAssistantCompletionSignature,
|
|
28
28
|
matchesCompactIntelligenceControlLabel,
|
|
29
|
-
|
|
29
|
+
matchesModelConfigurationOpener,
|
|
30
30
|
matchesModelFamilyLabel,
|
|
31
31
|
matchesRequestedModelControlLabel,
|
|
32
32
|
requestedEffortLabel,
|
|
@@ -58,7 +58,7 @@ const jobPath = `${jobDir}/job.json`;
|
|
|
58
58
|
const CHATGPT_LABELS = {
|
|
59
59
|
composer: "Chat with ChatGPT",
|
|
60
60
|
addFiles: "Add files and more",
|
|
61
|
-
send: "Send prompt",
|
|
61
|
+
send: ["Send prompt", "Send"],
|
|
62
62
|
close: "Close",
|
|
63
63
|
autoSwitchToThinking: "Auto-switch to Thinking",
|
|
64
64
|
configure: "Configure...",
|
|
@@ -66,7 +66,7 @@ const CHATGPT_LABELS = {
|
|
|
66
66
|
const GROK_LABELS = {
|
|
67
67
|
composer: "Ask Grok anything",
|
|
68
68
|
addFiles: "Attach",
|
|
69
|
-
send: "Submit",
|
|
69
|
+
send: ["Submit"],
|
|
70
70
|
modelSelect: "Model select",
|
|
71
71
|
stop: "Stop model response",
|
|
72
72
|
};
|
|
@@ -916,18 +916,6 @@ function matchesRequestedModelControl(candidate, selection, options = {}) {
|
|
|
916
916
|
return matchesRequestedModelControlLabel(candidate.label, selection);
|
|
917
917
|
}
|
|
918
918
|
|
|
919
|
-
function matchesModelConfigurationOpener(candidate) {
|
|
920
|
-
if (candidate.kind !== "button" || typeof candidate.label !== "string" || candidate.disabled) return false;
|
|
921
|
-
const label = String(candidate.label || "");
|
|
922
|
-
return candidate.label === "Model"
|
|
923
|
-
|| candidate.label === "Model selector"
|
|
924
|
-
|| matchesCompactIntelligenceOpenerLabel(label)
|
|
925
|
-
|| /^(?:Light|Standard|Extended|Heavy)(?:, click to remove)?$/i.test(label)
|
|
926
|
-
|| ["instant", "thinking", "pro"].some((family) => matchesModelFamilyLabel(label, /** @type {import("./chatgpt-ui-helpers.d.mts").OracleUiModelFamily} */ (family)))
|
|
927
|
-
|| /^(?:(?:Light|Standard|Extended|Heavy) )?Thinking(?:, click to remove)?$/i.test(label)
|
|
928
|
-
|| /^(?:(?:Light|Standard|Extended|Heavy) )?Pro(?:, click to remove)?$/i.test(label);
|
|
929
|
-
}
|
|
930
|
-
|
|
931
919
|
function canUseOpenModelMenuForSelection(snapshot, selection) {
|
|
932
920
|
if (selection.modelFamily !== "instant" || selection.autoSwitchToThinking === true) return false;
|
|
933
921
|
return Boolean(findEntry(
|
|
@@ -1312,7 +1300,7 @@ async function waitForUploadConfirmed(job, fileLabel, baselineCount) {
|
|
|
1312
1300
|
const labels = labelsForJob(job);
|
|
1313
1301
|
const sendEntry = findEntry(
|
|
1314
1302
|
snapshot,
|
|
1315
|
-
(candidate) => candidate.kind === "button" && candidate.label
|
|
1303
|
+
(candidate) => candidate.kind === "button" && labels.send.includes(candidate.label || "") && !candidate.disabled,
|
|
1316
1304
|
);
|
|
1317
1305
|
const fileCount = isGrokJob(job) && snapshot.includes(fileLabel)
|
|
1318
1306
|
? baselineCount + 1
|
|
@@ -1346,12 +1334,12 @@ async function waitForSendReady(job) {
|
|
|
1346
1334
|
const labels = labelsForJob(job);
|
|
1347
1335
|
const entry = findEntry(
|
|
1348
1336
|
snapshot,
|
|
1349
|
-
(candidate) => candidate.kind === "button" && candidate.label
|
|
1337
|
+
(candidate) => candidate.kind === "button" && labels.send.includes(candidate.label || "") && !candidate.disabled,
|
|
1350
1338
|
);
|
|
1351
1339
|
if (entry) return entry;
|
|
1352
1340
|
await sleep(1000);
|
|
1353
1341
|
}
|
|
1354
|
-
throw new Error(`Timed out waiting for ${labelsForJob(job).send} to become enabled`);
|
|
1342
|
+
throw new Error(`Timed out waiting for ${labelsForJob(job).send.join(" or ")} to become enabled`);
|
|
1355
1343
|
}
|
|
1356
1344
|
|
|
1357
1345
|
async function activateSendButton(job) {
|
|
@@ -1360,12 +1348,12 @@ async function activateSendButton(job) {
|
|
|
1360
1348
|
const buttons = Array.from(document.querySelectorAll('button'));
|
|
1361
1349
|
const button = buttons.find((candidate) => {
|
|
1362
1350
|
const label = (candidate.getAttribute('aria-label') || candidate.textContent || '').trim();
|
|
1363
|
-
return
|
|
1351
|
+
return labels.send.includes(label);
|
|
1364
1352
|
});
|
|
1365
1353
|
if (!button) return { ok: false, reason: 'send button not found' };
|
|
1366
1354
|
if (button.disabled || button.getAttribute('aria-disabled') === 'true') return { ok: false, reason: 'send button disabled' };
|
|
1367
1355
|
button.click();
|
|
1368
|
-
return { ok: true };
|
|
1356
|
+
return { ok: true, label: (button.getAttribute('aria-label') || button.textContent || '').trim() };
|
|
1369
1357
|
`));
|
|
1370
1358
|
return result;
|
|
1371
1359
|
}
|
|
@@ -1380,7 +1368,7 @@ async function sendAcceptanceState(job, baselineAssistantCount) {
|
|
|
1380
1368
|
url: urlResult.url,
|
|
1381
1369
|
urlKnown: urlResult.ok,
|
|
1382
1370
|
assistantCount: Math.max(baselineAssistantCount, messages.length),
|
|
1383
|
-
stopStreaming: isGrokJob(job) ? snapshot.includes(GROK_LABELS.stop) : snapshot.includes("Stop streaming"),
|
|
1371
|
+
stopStreaming: isGrokJob(job) ? snapshot.includes(GROK_LABELS.stop) : snapshot.includes("Stop streaming") || snapshot.includes('button "Stop"'),
|
|
1384
1372
|
transientErrorText: detectProviderVisibleBlockerText(snapshot) || "",
|
|
1385
1373
|
};
|
|
1386
1374
|
}
|
|
@@ -1389,12 +1377,13 @@ async function clickSend(job, baselineAssistantCount) {
|
|
|
1389
1377
|
await waitForSendReady(job);
|
|
1390
1378
|
const beforeSend = await sendAcceptanceState(job, baselineAssistantCount);
|
|
1391
1379
|
const activation = await activateSendButton(job);
|
|
1392
|
-
|
|
1393
|
-
|
|
1380
|
+
const sendLabel = activation?.label || labelsForJob(job).send.join(" or ");
|
|
1381
|
+
if (!activation?.ok) throw new Error(`Could not activate ${sendLabel}: ${activation?.reason || "DOM activation failed"}`);
|
|
1382
|
+
await log(`Activated ${sendLabel}; waiting for provider acceptance evidence`);
|
|
1394
1383
|
if (await waitForSendAccepted(job, beforeSend, { timeoutMs: 20_000 })) return;
|
|
1395
1384
|
|
|
1396
1385
|
await captureDiagnostics(job, "send-not-accepted");
|
|
1397
|
-
throw new Error(`${isGrokJob(job) ? "Grok" : "ChatGPT"} message did not leave the composer after activating ${
|
|
1386
|
+
throw new Error(`${isGrokJob(job) ? "Grok" : "ChatGPT"} message did not leave the composer after activating ${sendLabel}`);
|
|
1398
1387
|
}
|
|
1399
1388
|
|
|
1400
1389
|
async function waitForSendAccepted(job, beforeSend, options = {}) {
|
|
@@ -1534,7 +1523,7 @@ async function waitForModelConfigurationToSettle(job, options = {}) {
|
|
|
1534
1523
|
throw new Error(`Could not verify requested model settings after configuration for ${job.selection.modelFamily}`);
|
|
1535
1524
|
}
|
|
1536
1525
|
|
|
1537
|
-
async function configurePowerSlider(job) {
|
|
1526
|
+
async function configurePowerSlider(job, powerRef) {
|
|
1538
1527
|
// The current picker retains the compact tier semantics, but exposes keyboard
|
|
1539
1528
|
// interaction on a Power menuitem rather than accessible tier radio buttons.
|
|
1540
1529
|
const tiers = ["Instant", "Medium", "High", "Extra High", "Pro"];
|
|
@@ -1543,10 +1532,10 @@ async function configurePowerSlider(job) {
|
|
|
1543
1532
|
: job.selection.effort === "heavy" ? "Extra High"
|
|
1544
1533
|
: job.selection.effort === "extended" ? "High" : "Medium";
|
|
1545
1534
|
const target = tiers.indexOf(requested);
|
|
1546
|
-
const selector = '[
|
|
1547
|
-
await agentBrowser(job, "focus", selector);
|
|
1535
|
+
const selector = '[role="menuitem"][aria-label="Power"]';
|
|
1548
1536
|
const timeoutAt = Date.now() + MODEL_CONFIGURATION_SETTLE_TIMEOUT_MS;
|
|
1549
1537
|
let moves = 0;
|
|
1538
|
+
let focused = false;
|
|
1550
1539
|
while (Date.now() < timeoutAt && moves < tiers.length) {
|
|
1551
1540
|
const state = await evalPage(job, toJsonScript(`
|
|
1552
1541
|
const control = document.querySelector(${JSON.stringify(selector)});
|
|
@@ -1566,6 +1555,10 @@ async function configurePowerSlider(job) {
|
|
|
1566
1555
|
if (state.min !== "0" || state.max !== "4" || typeof state.value !== "string" || !Number.isInteger(value) || value < 0 || value > 4) {
|
|
1567
1556
|
throw new Error("ChatGPT Power slider has an unrecognized range; refusing to guess model settings");
|
|
1568
1557
|
}
|
|
1558
|
+
if (!focused) {
|
|
1559
|
+
await agentBrowser(job, "focus", powerRef);
|
|
1560
|
+
focused = true;
|
|
1561
|
+
}
|
|
1569
1562
|
if (value === target) {
|
|
1570
1563
|
if (!state.description.startsWith(`${requested}, ${target + 1} of ${tiers.length}.`)) {
|
|
1571
1564
|
throw new Error(`Could not verify ChatGPT Power tier: ${requested}`);
|
|
@@ -1592,8 +1585,9 @@ async function configureModel(job) {
|
|
|
1592
1585
|
|
|
1593
1586
|
await log(`Configuring model family=${job.selection.modelFamily} effort=${job.selection?.effort || "(none)"}`);
|
|
1594
1587
|
let familySnapshot = await openModelConfiguration(job);
|
|
1595
|
-
|
|
1596
|
-
|
|
1588
|
+
const powerEntry = findEntry(familySnapshot, (entry) => entry.kind === "menuitem" && entry.label === "Power" && !entry.disabled);
|
|
1589
|
+
if (powerEntry) {
|
|
1590
|
+
await configurePowerSlider(job, powerEntry.ref);
|
|
1597
1591
|
return;
|
|
1598
1592
|
}
|
|
1599
1593
|
let verificationSnapshot = familySnapshot;
|
|
@@ -1776,7 +1770,7 @@ async function assistantMessages(job) {
|
|
|
1776
1770
|
job,
|
|
1777
1771
|
toJsonScript(`
|
|
1778
1772
|
const headings = Array.from(document.querySelectorAll('h1,h2,h3,h4,h5,h6,[role="heading"]'))
|
|
1779
|
-
.filter((el) => (el.textContent || '').trim() === 'ChatGPT said:');
|
|
1773
|
+
.filter((el) => el.getAttribute('data-conversation-role') === 'assistant' || (el.textContent || '').trim() === 'ChatGPT said:');
|
|
1780
1774
|
const renderText = (node) => {
|
|
1781
1775
|
if (!node) return '';
|
|
1782
1776
|
const clone = node.cloneNode(true);
|
|
@@ -1802,7 +1796,10 @@ async function assistantMessages(job) {
|
|
|
1802
1796
|
.trim();
|
|
1803
1797
|
return text;
|
|
1804
1798
|
};
|
|
1805
|
-
const headingMessages = headings.map((heading) => ({
|
|
1799
|
+
const headingMessages = headings.map((heading) => ({
|
|
1800
|
+
text: renderText(heading.nextElementSibling),
|
|
1801
|
+
turnComplete: heading.closest('[data-talvt-turn-state]')?.getAttribute('data-talvt-turn-state') === 'complete',
|
|
1802
|
+
}));
|
|
1806
1803
|
const messageNodes = Array.from(document.querySelectorAll('[data-testid="assistant-message"], [data-message-author-role="assistant"]'));
|
|
1807
1804
|
const nodeMessages = messageNodes.map((node) => ({ text: renderText(node) }));
|
|
1808
1805
|
return {
|
|
@@ -1812,7 +1809,10 @@ async function assistantMessages(job) {
|
|
|
1812
1809
|
);
|
|
1813
1810
|
|
|
1814
1811
|
if (!Array.isArray(result?.messages)) return [];
|
|
1815
|
-
return result.messages.map((message) => ({
|
|
1812
|
+
return result.messages.map((message) => ({
|
|
1813
|
+
text: typeof message?.text === "string" ? message.text : "",
|
|
1814
|
+
turnComplete: message?.turnComplete === true,
|
|
1815
|
+
}));
|
|
1816
1816
|
}
|
|
1817
1817
|
|
|
1818
1818
|
async function grokAssistantMessages(job) {
|
|
@@ -1881,7 +1881,7 @@ async function waitForChatCompletion(job, baselineAssistantCount) {
|
|
|
1881
1881
|
while (Date.now() < timeoutAt) {
|
|
1882
1882
|
await heartbeat();
|
|
1883
1883
|
const [snapshot, body] = await Promise.all([snapshotText(job), pageText(job).catch(() => "")]);
|
|
1884
|
-
const hasStopStreaming = isGrokJob(job) ? snapshot.includes(GROK_LABELS.stop) : snapshot.includes("Stop streaming");
|
|
1884
|
+
const hasStopStreaming = isGrokJob(job) ? snapshot.includes(GROK_LABELS.stop) : snapshot.includes("Stop streaming") || snapshot.includes('button "Stop"');
|
|
1885
1885
|
const hasRetryButton = snapshot.includes('button "Retry"');
|
|
1886
1886
|
const copyResponseCount = isGrokJob(job) ? (snapshot.match(/button "Copy"/g) || []).length : (snapshot.match(/Copy response/g) || []).length;
|
|
1887
1887
|
throwIfProviderTransientError(job, snapshot, "waiting for response completion");
|
|
@@ -1889,7 +1889,7 @@ async function waitForChatCompletion(job, baselineAssistantCount) {
|
|
|
1889
1889
|
const messages = await assistantMessages(job);
|
|
1890
1890
|
const targetMessage = messages[baselineAssistantCount];
|
|
1891
1891
|
const targetText = targetMessage?.text || "";
|
|
1892
|
-
const hasTargetCopyResponse = copyResponseCount > baselineAssistantCount;
|
|
1892
|
+
const hasTargetCopyResponse = copyResponseCount > baselineAssistantCount || targetMessage?.turnComplete === true;
|
|
1893
1893
|
|
|
1894
1894
|
if (!hasStopStreaming && hasRetryButton && responseFailureText) {
|
|
1895
1895
|
if (!retriedAfterFailure) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-oracle",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.23",
|
|
4
4
|
"description": "ChatGPT and Grok web-oracle extension for pi with isolated browser auth, async jobs, and project-context archives.",
|
|
5
5
|
"private": false,
|
|
6
6
|
"license": "MIT",
|
|
@@ -38,6 +38,7 @@
|
|
|
38
38
|
"scripts/platform-smoke",
|
|
39
39
|
"scripts/oracle-real-smoke.mjs",
|
|
40
40
|
"scripts/oracle-auth.test.mjs",
|
|
41
|
+
"scripts/oracle-cookie-import.test.mjs",
|
|
41
42
|
"scripts/oracle-model.test.mjs",
|
|
42
43
|
"scripts/oracle-chatgpt-preset-proof.mjs"
|
|
43
44
|
],
|
|
@@ -50,10 +51,10 @@
|
|
|
50
51
|
]
|
|
51
52
|
},
|
|
52
53
|
"scripts": {
|
|
53
|
-
"check:oracle-extension": "node --check extensions/oracle/shared/browser-profile-helpers.mjs && node --check extensions/oracle/shared/process-helpers.mjs && node --check extensions/oracle/shared/state-path-helpers.mjs && node --check extensions/oracle/shared/time-helpers.mjs && node --check extensions/oracle/shared/state-coordination-helpers.mjs && node --check extensions/oracle/shared/job-coordination-helpers.mjs && node --check extensions/oracle/shared/job-lifecycle-helpers.mjs && node --check extensions/oracle/shared/job-observability-helpers.mjs && node --check extensions/oracle/worker/run-job.mjs && node --check extensions/oracle/worker/state-locks.mjs && node --check extensions/oracle/worker/artifact-heuristics.mjs && node --check extensions/oracle/worker/chatgpt-ui-helpers.mjs && node --check extensions/oracle/worker/chatgpt-flow-helpers.mjs && node --check extensions/oracle/worker/auth-flow-helpers.mjs && node --check extensions/oracle/worker/auth-cookie-policy.mjs && node --check extensions/oracle/worker/chromium-cookie-source.mjs && node --check extensions/oracle/worker/auth-bootstrap.mjs && esbuild extensions/oracle/index.ts --bundle --platform=node --format=esm --external:@earendil-works/pi-ai --external:@earendil-works/pi-coding-agent --external:typebox --outfile=/tmp/pi-oracle-extension-check.js",
|
|
54
|
+
"check:oracle-extension": "node --check extensions/oracle/shared/browser-profile-helpers.mjs && node --check extensions/oracle/shared/process-helpers.mjs && node --check extensions/oracle/shared/state-path-helpers.mjs && node --check extensions/oracle/shared/time-helpers.mjs && node --check extensions/oracle/shared/state-coordination-helpers.mjs && node --check extensions/oracle/shared/job-coordination-helpers.mjs && node --check extensions/oracle/shared/job-lifecycle-helpers.mjs && node --check extensions/oracle/shared/job-observability-helpers.mjs && node --check extensions/oracle/worker/run-job.mjs && node --check extensions/oracle/worker/state-locks.mjs && node --check extensions/oracle/worker/artifact-heuristics.mjs && node --check extensions/oracle/worker/chatgpt-ui-helpers.mjs && node --check extensions/oracle/worker/chatgpt-flow-helpers.mjs && node --check extensions/oracle/worker/auth-flow-helpers.mjs && node --check extensions/oracle/worker/auth-cookie-policy.mjs && node --check extensions/oracle/worker/chromium-cookie-source.mjs && node --check extensions/oracle/worker/linux-chromium-cookie-source.mjs && node --check extensions/oracle/worker/auth-bootstrap.mjs && esbuild extensions/oracle/index.ts --bundle --platform=node --format=esm --external:@earendil-works/pi-ai --external:@earendil-works/pi-coding-agent --external:typebox --outfile=/tmp/pi-oracle-extension-check.js",
|
|
54
55
|
"typecheck": "tsc --noEmit -p tsconfig.json",
|
|
55
56
|
"typecheck:worker-helpers": "tsc --noEmit -p tsconfig.worker-helpers.json",
|
|
56
|
-
"sanity:oracle": "node --test scripts/oracle-auth.test.mjs scripts/oracle-model.test.mjs && node scripts/oracle-sanity-runner.mjs",
|
|
57
|
+
"sanity:oracle": "node --test scripts/oracle-auth.test.mjs scripts/oracle-cookie-import.test.mjs scripts/oracle-model.test.mjs && node scripts/oracle-sanity-runner.mjs",
|
|
57
58
|
"pack:check": "npm pack --dry-run",
|
|
58
59
|
"verify:oracle": "npm run check:oracle-extension && npm run check:platform-smoke && npm run check:oracle-real-smoke && npm run check:oracle-release-proof && npm run typecheck && npm run typecheck:worker-helpers && npm run sanity:oracle && npm run pack:check",
|
|
59
60
|
"test": "npm run verify:oracle",
|
|
@@ -74,7 +75,7 @@
|
|
|
74
75
|
"release:proof:chatgpt-presets": "node scripts/oracle-chatgpt-preset-proof.mjs check",
|
|
75
76
|
"smoke:real:packed": "node scripts/oracle-real-smoke.mjs run --mode packed",
|
|
76
77
|
"smoke:real:source": "node scripts/oracle-real-smoke.mjs run --mode source",
|
|
77
|
-
"sanity:oracle:platform": "node --test scripts/oracle-auth.test.mjs scripts/oracle-model.test.mjs && node scripts/oracle-sanity-runner.mjs --mode platform",
|
|
78
|
+
"sanity:oracle:platform": "node --test scripts/oracle-auth.test.mjs scripts/oracle-cookie-import.test.mjs scripts/oracle-model.test.mjs && node scripts/oracle-sanity-runner.mjs --mode platform",
|
|
78
79
|
"verify:oracle:platform": "npm run check:oracle-extension && npm run check:platform-smoke && npm run check:oracle-real-smoke && npm run check:oracle-release-proof && npm run sanity:oracle:platform && npm run pack:check"
|
|
79
80
|
},
|
|
80
81
|
"dependencies": {
|
|
@@ -86,17 +87,17 @@
|
|
|
86
87
|
"typebox": "*"
|
|
87
88
|
},
|
|
88
89
|
"overrides": {
|
|
89
|
-
"basic-ftp": "6.
|
|
90
|
-
"protobufjs": "7.6.
|
|
90
|
+
"basic-ftp": "6.2.3",
|
|
91
|
+
"protobufjs": "7.6.6"
|
|
91
92
|
},
|
|
92
93
|
"devDependencies": {
|
|
93
|
-
"@earendil-works/pi-ai": "0.
|
|
94
|
-
"@earendil-works/pi-coding-agent": "0.
|
|
94
|
+
"@earendil-works/pi-ai": "1.0.0",
|
|
95
|
+
"@earendil-works/pi-coding-agent": "1.0.0",
|
|
95
96
|
"@types/node": "^22.19.19",
|
|
96
97
|
"esbuild": "^0.28.0",
|
|
97
98
|
"tsx": "^4.22.3",
|
|
98
|
-
"typebox": "
|
|
99
|
-
"typescript": "^
|
|
99
|
+
"typebox": "1.3.34",
|
|
100
|
+
"typescript": "^7.0.0"
|
|
100
101
|
},
|
|
101
102
|
"engines": {
|
|
102
103
|
"node": ">=22.19.0"
|
|
@@ -106,7 +107,7 @@
|
|
|
106
107
|
"linux",
|
|
107
108
|
"win32"
|
|
108
109
|
],
|
|
109
|
-
"packageManager": "npm@11.
|
|
110
|
+
"packageManager": "npm@11.21.0",
|
|
110
111
|
"peerDependenciesMeta": {
|
|
111
112
|
"@earendil-works/pi-ai": {
|
|
112
113
|
"optional": true
|
|
@@ -6,33 +6,12 @@ export default {
|
|
|
6
6
|
artifactRoot: ".artifacts/platform-smoke",
|
|
7
7
|
requiredTargets: ["macos", "ubuntu", "windows-native"],
|
|
8
8
|
requiredSuites: ["platform-build", "real-extension"],
|
|
9
|
-
workflows: {
|
|
10
|
-
everyday: {
|
|
11
|
-
description: "Fast local validation for normal iteration.",
|
|
12
|
-
commands: ["npm run verify:oracle"],
|
|
13
|
-
},
|
|
14
|
-
platformSensitive: {
|
|
15
|
-
description: "Doctor plus focused platform target/suite runs for platform-sensitive changes.",
|
|
16
|
-
commands: [
|
|
17
|
-
"npm run smoke:platform:doctor",
|
|
18
|
-
"node scripts/platform-smoke.mjs run --target <target> --suite <suite>",
|
|
19
|
-
],
|
|
20
|
-
},
|
|
21
|
-
platformMatrix: {
|
|
22
|
-
description: "Doctor-first packed-install macOS/Ubuntu/Windows platform proof.",
|
|
23
|
-
commands: ["npm run smoke:platform:all"],
|
|
24
|
-
},
|
|
25
|
-
release: {
|
|
26
|
-
description: "Full release gate: local verification, fresh ChatGPT preset proof, plus the doctor-first platform matrix.",
|
|
27
|
-
commands: ["npm run release:check"],
|
|
28
|
-
},
|
|
29
|
-
},
|
|
30
9
|
requiredCrabbox: {
|
|
31
10
|
source: "https://github.com/openclaw/crabbox",
|
|
32
11
|
minVersion: "0.26.0",
|
|
33
12
|
},
|
|
34
13
|
ubuntuContainerImage: "pi-oracle-platform-smoke:node24",
|
|
35
|
-
ubuntuContainerBaseImage: "cimg/node:24.
|
|
14
|
+
ubuntuContainerBaseImage: "cimg/node:24.21",
|
|
36
15
|
windowsParallels: {
|
|
37
16
|
sourceVm: "pi-extension-windows-template",
|
|
38
17
|
snapshot: "crabbox-ready",
|
|
@@ -46,6 +46,41 @@ function pollingHarness(overrides = {}) {
|
|
|
46
46
|
return { poll, logs, get captures() { return captures; } };
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
test("auth launch waits for a closing daemon only on the known pre-launch socket failure", async () => {
|
|
50
|
+
const socketFailure = "Could not configure browser: Failed to connect: No such file or directory (os error 2)";
|
|
51
|
+
for (const [failures, message, succeeds] of [
|
|
52
|
+
[1, socketFailure, true],
|
|
53
|
+
[Infinity, socketFailure, false],
|
|
54
|
+
[1, "Chrome exited before startup", false],
|
|
55
|
+
]) {
|
|
56
|
+
let now = 0;
|
|
57
|
+
let launches = 0;
|
|
58
|
+
const launch = vm.runInNewContext(`(${functionSource(bootstrap, "launchTargetBrowser")})`, {
|
|
59
|
+
Date: { now: () => now }, Math, Error, JSON,
|
|
60
|
+
AGENT_BROWSER_BIN: "agent-browser", AGENT_BROWSER_COMMAND_TIMEOUT_MS: 500,
|
|
61
|
+
closeTargetBrowser: async () => {},
|
|
62
|
+
targetBrowserBaseArgs: () => ["--session", "fixture-auth", "--profile", "/fixture/seed"],
|
|
63
|
+
log: async () => {},
|
|
64
|
+
sleep: async (ms) => { now += ms; },
|
|
65
|
+
spawnCommand: async (_command, args, options) => {
|
|
66
|
+
assert.equal(args.at(-1), "about:blank");
|
|
67
|
+
assert(options.timeoutMs > 0 && options.timeoutMs <= 500);
|
|
68
|
+
return ++launches <= failures
|
|
69
|
+
? { code: 1, stdout: "", stderr: message }
|
|
70
|
+
: { code: 0, stdout: "Browser opened", stderr: "" };
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
if (succeeds) {
|
|
74
|
+
await launch();
|
|
75
|
+
assert.equal(launches, 2);
|
|
76
|
+
} else {
|
|
77
|
+
await assert.rejects(launch(), (error) => error.message === message);
|
|
78
|
+
if (message === socketFailure) assert(now <= 500 && launches > 1, "persistent socket failure must stop at the launch deadline");
|
|
79
|
+
else assert.equal(launches, 1, "other launch failures must not be retried");
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
|
|
49
84
|
test("auth polling retries a complete observation after each navigation-interrupted read", async () => {
|
|
50
85
|
for (const read of ["getUrl", "snapshotText", "pageText", "loginProbe"]) {
|
|
51
86
|
let calls = 0;
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
import { execFileSync } from "node:child_process";
|
|
8
8
|
import { existsSync, readFileSync } from "node:fs";
|
|
9
|
+
import { createRequire } from "node:module";
|
|
9
10
|
import { dirname, resolve } from "node:path";
|
|
10
11
|
import { fileURLToPath } from "node:url";
|
|
11
12
|
|
|
@@ -60,8 +61,25 @@ function currentGitHead() {
|
|
|
60
61
|
return git(["rev-parse", "HEAD"]);
|
|
61
62
|
}
|
|
62
63
|
|
|
63
|
-
function
|
|
64
|
-
return git(["show", "-s", "--format=%cI",
|
|
64
|
+
function gitCommitCommittedAt(ref = "HEAD") {
|
|
65
|
+
return git(["show", "-s", "--format=%cI", ref]);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function isRuntimeIdenticalAncestor(candidateHead, currentHead) {
|
|
69
|
+
if (typeof candidateHead !== "string" || !/^[0-9a-f]{40}$/i.test(candidateHead)) return false;
|
|
70
|
+
if (candidateHead === currentHead) return true;
|
|
71
|
+
try {
|
|
72
|
+
execFileSync("git", ["merge-base", "--is-ancestor", candidateHead, currentHead], { cwd: REPO_ROOT, stdio: "ignore" });
|
|
73
|
+
execFileSync("git", ["diff", "--exit-code", candidateHead, currentHead, "--", "extensions", "prompts"], { cwd: REPO_ROOT, stdio: "ignore" });
|
|
74
|
+
return true;
|
|
75
|
+
} catch {
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function packageMetadataAtCommit(ref) {
|
|
81
|
+
const pkg = JSON.parse(git(["show", `${ref}:package.json`]));
|
|
82
|
+
return { name: pkg.name, version: pkg.version };
|
|
65
83
|
}
|
|
66
84
|
|
|
67
85
|
function currentGitStatus() {
|
|
@@ -69,18 +87,24 @@ function currentGitStatus() {
|
|
|
69
87
|
}
|
|
70
88
|
|
|
71
89
|
function canonicalPresets() {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
autoSwitchToThinking:
|
|
83
|
-
|
|
90
|
+
// Derive the canonical registry by importing the real config module so a
|
|
91
|
+
// formatting change in config.ts can never silently drop a preset from the
|
|
92
|
+
// release proof's required set.
|
|
93
|
+
const tsx = createRequire(import.meta.url).resolve("tsx");
|
|
94
|
+
const stdout = execFileSync(process.execPath, [
|
|
95
|
+
"--import", tsx,
|
|
96
|
+
"--input-type=module",
|
|
97
|
+
"--eval",
|
|
98
|
+
"const mod = await import('./extensions/oracle/lib/config.ts');" +
|
|
99
|
+
"const presets = Object.fromEntries(Object.entries(mod.ORACLE_SUBMIT_PRESETS).map(([id, preset]) => [id, {" +
|
|
100
|
+
"modelFamily: preset.modelFamily, effort: preset.effort, autoSwitchToThinking: preset.autoSwitchToThinking }]));" +
|
|
101
|
+
"process.stdout.write(JSON.stringify(presets));",
|
|
102
|
+
], { cwd: REPO_ROOT, encoding: "utf8" });
|
|
103
|
+
const presets = JSON.parse(stdout);
|
|
104
|
+
if (!presets || typeof presets !== "object" || Object.keys(presets).length === 0) {
|
|
105
|
+
throw new Error("ORACLE_SUBMIT_PRESETS registry resolved to no presets");
|
|
106
|
+
}
|
|
107
|
+
return presets;
|
|
84
108
|
}
|
|
85
109
|
|
|
86
110
|
function canonicalPresetIds() {
|
|
@@ -223,8 +247,6 @@ function validateProof(proof, path) {
|
|
|
223
247
|
const errors = [];
|
|
224
248
|
const { name, version } = packageMetadata();
|
|
225
249
|
const gitHead = currentGitHead();
|
|
226
|
-
const gitHeadCommittedAt = currentGitHeadCommittedAt();
|
|
227
|
-
const gitHeadCommittedAtMs = Date.parse(gitHeadCommittedAt);
|
|
228
250
|
const gitStatus = currentGitStatus();
|
|
229
251
|
const presetRegistry = canonicalPresets();
|
|
230
252
|
const requiredPresets = Object.keys(presetRegistry);
|
|
@@ -239,10 +261,17 @@ function validateProof(proof, path) {
|
|
|
239
261
|
return errors;
|
|
240
262
|
}
|
|
241
263
|
|
|
264
|
+
const proofGitHead = typeof proof.gitHead === "string" ? proof.gitHead : "";
|
|
265
|
+
const validProofHead = isRuntimeIdenticalAncestor(proofGitHead, gitHead);
|
|
266
|
+
const expectedProofHead = validProofHead ? proofGitHead : gitHead;
|
|
267
|
+
const expectedVersion = validProofHead ? packageMetadataAtCommit(expectedProofHead).version : version;
|
|
268
|
+
const gitHeadCommittedAt = gitCommitCommittedAt(expectedProofHead);
|
|
269
|
+
const gitHeadCommittedAtMs = Date.parse(gitHeadCommittedAt);
|
|
270
|
+
|
|
242
271
|
if (proof.schemaVersion !== 1) errors.push("schemaVersion must be 1");
|
|
243
272
|
if (proof.packageName !== name) errors.push(`packageName must be ${name}`);
|
|
244
|
-
if (proof.packageVersion !==
|
|
245
|
-
if (
|
|
273
|
+
if (proof.packageVersion !== expectedVersion) errors.push(`packageVersion must match package.json version ${expectedVersion}`);
|
|
274
|
+
if (!validProofHead) errors.push(`gitHead must match current HEAD ${gitHead} or a runtime-identical ancestor commit`);
|
|
246
275
|
if (proof.provider !== "chatgpt") errors.push('provider must be "chatgpt"');
|
|
247
276
|
if (proof.extensionUnderTest !== "loaded-extension") errors.push('extensionUnderTest must be "loaded-extension"');
|
|
248
277
|
|
|
@@ -255,7 +284,7 @@ function validateProof(proof, path) {
|
|
|
255
284
|
const maxAgeMs = maxAgeHours() * 60 * 60 * 1000;
|
|
256
285
|
if (ageMs < 0) errors.push("validatedAt must not be in the future");
|
|
257
286
|
if (ageMs > maxAgeMs) errors.push(`validatedAt is older than ${maxAgeHours()} hours`);
|
|
258
|
-
if (proofValidatedAtMs <= gitHeadCommittedAtMs) errors.push("validatedAt must be after
|
|
287
|
+
if (proofValidatedAtMs <= gitHeadCommittedAtMs) errors.push("validatedAt must be after proven git commit time");
|
|
259
288
|
}
|
|
260
289
|
|
|
261
290
|
const jobs = proof.jobs;
|
|
@@ -270,8 +299,8 @@ function validateProof(proof, path) {
|
|
|
270
299
|
canonicalPreset: presetRegistry[preset],
|
|
271
300
|
proofJob: jobs[preset],
|
|
272
301
|
packageName: name,
|
|
273
|
-
packageVersion:
|
|
274
|
-
gitHead,
|
|
302
|
+
packageVersion: expectedVersion,
|
|
303
|
+
gitHead: expectedProofHead,
|
|
275
304
|
gitHeadCommittedAtMs,
|
|
276
305
|
proofValidatedAtMs,
|
|
277
306
|
errors,
|