pi-oracle 0.7.22 → 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 +29 -0
- package/README.md +2 -2
- package/docs/development.md +4 -4
- package/docs/platform-smoke.md +15 -4
- package/docs/reference.md +7 -0
- 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/run-job.mjs +24 -29
- package/package.json +2 -2
- package/scripts/oracle-chatgpt-preset-proof.mjs +31 -9
- package/scripts/oracle-model.test.mjs +19 -16
- package/scripts/oracle-real-smoke.mjs +13 -2
- package/scripts/platform-smoke/Dockerfile.ubuntu +2 -2
- package/scripts/platform-smoke/crabbox-runner.mjs +15 -1
- package/scripts/platform-smoke/doctor.mjs +52 -20
- package/scripts/platform-smoke/invariants.mjs +24 -8
- package/scripts/platform-smoke/platform-build-windows.ps1 +17 -1
- package/scripts/platform-smoke/targets.mjs +24 -3
- package/scripts/platform-smoke.mjs +45 -12
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,34 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## Unreleased
|
|
4
|
+
|
|
5
|
+
## 0.7.23 - 2026-11-02
|
|
6
|
+
|
|
7
|
+
<!-- npm-release:unreleased:start -->
|
|
8
|
+
### Changed
|
|
9
|
+
- update the `basic-ftp` dependency override to the patched stable release
|
|
10
|
+
- publish versioned npm candidates automatically after reviewed changes merge to `main` and pass Pi compatibility checks
|
|
11
|
+
- support target-specific platform preflight for distributed release checks
|
|
12
|
+
- qualify retained Oracle release candidates across hosted macOS, Ubuntu, and native Windows Crabbox targets before publication
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
- recognize ChatGPT's current model selector, send button, and completed response state
|
|
16
|
+
<!-- npm-release:unreleased:end -->
|
|
17
|
+
|
|
18
|
+
- Automate qualified Oracle releases (3/3) (#72) ([596d8b7](https://github.com/fitchmultz/pi-oracle/commit/596d8b7c2c0591d80bdb1e8c260bc04b3cf16310))
|
|
19
|
+
|
|
20
|
+
### What's Changed
|
|
21
|
+
* ci: publish npm releases automatically on merge by @fitchmultz in https://github.com/fitchmultz/pi-oracle/pull/64
|
|
22
|
+
* chore(deps): update dependency basic-ftp to v6.2.3 by @renovate[bot] in https://github.com/fitchmultz/pi-oracle/pull/55
|
|
23
|
+
* Restore Oracle ChatGPT flow and prepare release (1/2) by @fitchmultz in https://github.com/fitchmultz/pi-oracle/pull/68
|
|
24
|
+
* Qualify retained candidates on native SSH targets (2/2) by @fitchmultz in https://github.com/fitchmultz/pi-oracle/pull/69
|
|
25
|
+
* Support target-specific platform preflight (1/3) by @fitchmultz in https://github.com/fitchmultz/pi-oracle/pull/70
|
|
26
|
+
* Qualify retained Oracle packages on hosted platforms (2/3) by @fitchmultz in https://github.com/fitchmultz/pi-oracle/pull/71
|
|
27
|
+
* Automate qualified Oracle releases (3/3) by @fitchmultz in https://github.com/fitchmultz/pi-oracle/pull/72
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
**Full Changelog**: https://github.com/fitchmultz/pi-oracle/compare/v0.7.22...v0.7.23
|
|
31
|
+
|
|
3
32
|
## 0.7.22 - 2026-10-12
|
|
4
33
|
|
|
5
34
|
### Documentation
|
package/README.md
CHANGED
|
@@ -6,9 +6,9 @@ pi-oracle sends project files and questions from [Pi](https://github.com/earendi
|
|
|
6
6
|
|
|
7
7
|
## Install
|
|
8
8
|
|
|
9
|
-
Use Node.js
|
|
9
|
+
Use a supported Node.js release on macOS, Linux, or Windows native. Use a current Pi release and a Chromium-family browser.
|
|
10
10
|
|
|
11
|
-
Install `agent-browser` and `tar`. Install `zstd` for ChatGPT jobs. Check the [platform requirements](docs/reference.md#requirements) for profile copies and encrypted cookies.
|
|
11
|
+
Install `agent-browser` and `tar`. Install `zstd` for ChatGPT jobs. Check the [platform requirements](docs/reference.md#requirements) for the Node.js minimum, profile copies, and encrypted cookies.
|
|
12
12
|
|
|
13
13
|
Install Oracle:
|
|
14
14
|
|
package/docs/development.md
CHANGED
|
@@ -12,11 +12,11 @@ Production hardening should keep focusing on UI drift detection, auth recovery,
|
|
|
12
12
|
|
|
13
13
|
## Automatic npm releases (maintainers)
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
The [shared release procedure](https://github.com/fitchmultz/.github#automatic-npm-releases) creates a reviewed patch-version PR after changes merge to `main`, then qualifies one retained candidate tarball across compatibility checks and hosted macOS, Ubuntu, and native Windows Crabbox suites before publishing the same version on npm and GitHub.
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
Before releasing runtime or prompt changes, verify [the `release:check` requirements](#verification) and [packed platform proof](platform-smoke.md). Fresh all-preset ChatGPT proof must match `HEAD` or an ancestor commit whose `extensions/` and `prompts/` trees are byte-identical to `HEAD`, satisfy the proof checker's 72-hour timing and saved-response requirements, and accompany the required doctor-first macOS/Ubuntu/native-Windows evidence. Authenticated and paid checks remain outside CI. Automated publication disables npm hooks to publish the checked bytes; manual `npm publish` still runs its existing `prepublishOnly` gate.
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
Set repository variable `NPM_RELEASE_ENABLED` to anything other than `true` to stop new version and release plans; cancel pending runs separately when needed. Follow the shared procedure to retry an unpublished candidate. Workflow validation is not evidence of a completed real OIDC publication.
|
|
20
20
|
|
|
21
21
|
## Verification
|
|
22
22
|
|
|
@@ -66,7 +66,7 @@ node scripts/oracle-chatgpt-preset-proof.mjs template > .artifacts/chatgpt-prese
|
|
|
66
66
|
npm run release:proof:chatgpt-presets
|
|
67
67
|
```
|
|
68
68
|
|
|
69
|
-
The proof checker is intentionally part of `release:check`; it fails if the proof is missing, stale, tied to a
|
|
69
|
+
The proof checker is intentionally part of `release:check`; it fails if the proof is missing, stale, tied to a commit whose `extensions/` or `prompts/` differ from `HEAD`, references jobs that completed before the proven commit, or lacks actual persisted ChatGPT `.tar.zst` job state and response text for any canonical preset.
|
|
70
70
|
|
|
71
71
|
The real runtime suite defaults to deterministic installed-tool execution so platform proof stays bounded. Provider/model defaults remain `zai/glm-5.2` for doctor/config and for optional model-agent debugging; override with `PI_ORACLE_REAL_TEST_PROVIDER` and `PI_ORACLE_REAL_TEST_MODEL` when needed. For inner-loop source loading only, use `npm run smoke:real:source`; it is not release proof. Set `PI_ORACLE_REAL_TEST_MODEL_AGENT=1` only when debugging the slower model-agent path. The optional second real-agent negative symlink check is opt-in via `PI_ORACLE_REAL_TEST_NEGATIVE_SYMLINK=1`; `npm run sanity:oracle` covers archive/symlink rejection by default without adding another model-agent turn to the platform release gate.
|
|
72
72
|
|
package/docs/platform-smoke.md
CHANGED
|
@@ -29,9 +29,10 @@ crabbox providers
|
|
|
29
29
|
|
|
30
30
|
Target setup:
|
|
31
31
|
|
|
32
|
-
- macOS: Remote Login enabled; noninteractive `ssh $USER@localhost` works; `node`, `npm`, `git`, `tar`, `rsync`, `zstd`, and `agent-browser` are on the SSH PATH.
|
|
32
|
+
- macOS: Remote Login enabled; noninteractive `ssh $USER@localhost` works; `node`, `npm`, `git`, `tar`, `rsync`, `zstd`, and `agent-browser` are on the SSH PATH. Use `PI_ORACLE_SMOKE_MAC_PORT` or `PLATFORM_SMOKE_MAC_PORT` for a non-default SSH port.
|
|
33
33
|
- Ubuntu: Docker is running and the configured image (`PI_ORACLE_SMOKE_UBUNTU_IMAGE`, default `pi-oracle-platform-smoke:node24`) has `node`, `npm`, `git`, `tar`, `rsync`, `zstd`, and `agent-browser` on PATH. Build the local image when needed with `docker build -t pi-oracle-platform-smoke:node24 -f scripts/platform-smoke/Dockerfile.ubuntu .`.
|
|
34
|
-
- Windows native: Parallels has stopped source VM `pi-extension-windows-template` and the configured power-off snapshot (`crabbox-ready` by default for this repo). The template must have OpenSSH, PowerShell, Git, Node/npm, `tar`, `zstd`, and `agent-browser` on PATH. Do not bake API keys, browser sessions, project checkouts, `.pi` state, artifacts, or secrets into the template.
|
|
34
|
+
- Windows native: by default, Parallels has stopped source VM `pi-extension-windows-template` and the configured power-off snapshot (`crabbox-ready` by default for this repo). The template must have OpenSSH, PowerShell, Git, Node/npm, `tar`, `zstd`, and `agent-browser` on PATH. Do not bake API keys, browser sessions, project checkouts, `.pi` state, artifacts, or secrets into the template.
|
|
35
|
+
- Alternatively, set `PI_ORACLE_SMOKE_WINDOWS_HOST` to a native Windows SSH host, and set `PI_ORACLE_SMOKE_WINDOWS_USER`, `PI_ORACLE_SMOKE_WINDOWS_PORT` (default 22), and `PI_ORACLE_SMOKE_WINDOWS_NATIVE_WORK_ROOT` for that target as needed. Crabbox uses its SSH provider instead of Parallels; the target needs the same tools on its SSH PATH. The `PLATFORM_SMOKE_WINDOWS_*` aliases also work.
|
|
35
36
|
|
|
36
37
|
Real runtime suite auth:
|
|
37
38
|
|
|
@@ -47,7 +48,7 @@ Use the narrowest workflow that proves the change. Do not run the full platform
|
|
|
47
48
|
| Situation | Canonical command(s) | What it proves |
|
|
48
49
|
| --- | --- | --- |
|
|
49
50
|
| Everyday local iteration | `npm run verify:oracle` | Syntax, bundle, platform-smoke invariants, type checks, oracle sanity, and package dry-run pass locally. |
|
|
50
|
-
| Platform-sensitive change | `
|
|
51
|
+
| Platform-sensitive change | `node scripts/platform-smoke.mjs doctor --target <target>`, then `node scripts/platform-smoke.mjs run --target <target> --suite <suite>` | Target setup is ready and the affected platform/suite works without paying for unrelated targets. |
|
|
51
52
|
| Platform matrix proof | `npm run smoke:platform:all` | Doctor-first packed-install proof passes on every required target and suite. |
|
|
52
53
|
| ChatGPT preset release proof | `npm run release:proof:chatgpt-presets` | Fresh loaded-extension proof exists for every canonical ChatGPT preset. |
|
|
53
54
|
| Publish/release gate | `npm run release:check` | Local verification (`verify:oracle`) passes, fresh ChatGPT preset proof exists, then the doctor-first platform matrix passes. |
|
|
@@ -65,13 +66,19 @@ npm run smoke:platform:all
|
|
|
65
66
|
Focused commands:
|
|
66
67
|
|
|
67
68
|
```bash
|
|
68
|
-
|
|
69
|
+
node scripts/platform-smoke.mjs doctor --target macos
|
|
69
70
|
npm run smoke:platform:macos
|
|
71
|
+
node scripts/platform-smoke.mjs doctor --target ubuntu
|
|
70
72
|
npm run smoke:platform:ubuntu
|
|
73
|
+
node scripts/platform-smoke.mjs doctor --target windows-native
|
|
71
74
|
npm run smoke:platform:windows-native
|
|
72
75
|
node scripts/platform-smoke.mjs run --target windows-native --suite real-extension
|
|
73
76
|
```
|
|
74
77
|
|
|
78
|
+
`doctor --target` checks the selected targets' providers and tools; without it,
|
|
79
|
+
doctor still checks every required target. In split hosted jobs, run a scoped
|
|
80
|
+
doctor on each target before its suites.
|
|
81
|
+
|
|
75
82
|
Full release gate:
|
|
76
83
|
|
|
77
84
|
```bash
|
|
@@ -80,6 +87,10 @@ npm run release:check
|
|
|
80
87
|
|
|
81
88
|
`release:check` runs `verify:oracle`, then `release:proof:chatgpt-presets`, then `smoke:platform:all`, matching the release order: cheap harness checks, fresh live ChatGPT preset proof, doctor, full matrix, then artifact review. `prepublishOnly` runs `npm run release:check`.
|
|
82
89
|
|
|
90
|
+
For an already prepared release candidate, set `PI_ORACLE_RELEASE_TARBALL` to its local tarball before running the matrix. Set `PI_ORACLE_RELEASE_SHA256` to the prepared candidate's SHA-256 when one is available; the runner checks it before transfer. Each suite still checks that the source packs, but installs the retained candidate. Crabbox transfers a temporary copy and each target verifies its SHA-256 before use; the suite evidence records the verified hash.
|
|
91
|
+
|
|
92
|
+
The reusable [hosted platform qualification](../.github/workflows/release-platform.yml) runs each target on its native GitHub-hosted OS. It verifies the shared candidate receipt and source before and after both suites, and retains the per-target evidence. This is the platform portion of release qualification; the live ChatGPT preset proof is a separate required gate.
|
|
93
|
+
|
|
83
94
|
## What `platform-build` proves
|
|
84
95
|
|
|
85
96
|
On each required target, `platform-build`:
|
package/docs/reference.md
CHANGED
|
@@ -26,6 +26,13 @@ You need:
|
|
|
26
26
|
- a normal persisted `pi` session, not `pi --no-session`
|
|
27
27
|
- on Linux, encrypted Chromium cookies may also require `secret-tool` (GNOME/libsecret) or `kwallet-query` + `dbus-send` (KDE), unless a Chrome/Brave safe-storage password override is set for the auth run
|
|
28
28
|
|
|
29
|
+
`agent-browser` has its own Node.js requirements, which may be newer than Oracle's package minimum.
|
|
30
|
+
If npm blocks `agent-browser`'s install script, allow it when installing the global package:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
npm install -g --allow-scripts=agent-browser agent-browser
|
|
34
|
+
```
|
|
35
|
+
|
|
29
36
|
## What a successful run looks like
|
|
30
37
|
|
|
31
38
|
```text
|
|
@@ -16,6 +16,7 @@ export declare function matchesModelFamilyLabel(label: string | undefined, famil
|
|
|
16
16
|
export declare function matchesRequestedModelControlLabel(label: string | undefined, selection: OracleUiSelection): boolean;
|
|
17
17
|
export declare function matchesCompactIntelligenceControlLabel(label: string | undefined): boolean;
|
|
18
18
|
export declare function matchesCompactIntelligenceOpenerLabel(label: string | undefined): boolean;
|
|
19
|
+
export declare function matchesModelConfigurationOpener(entry: import("./artifact-heuristics.mjs").SnapshotEntry): boolean;
|
|
19
20
|
export declare function requestedEffortLabel(selection: OracleUiSelection): string | undefined;
|
|
20
21
|
export declare function effortSelectionVisible(snapshot: string, effortLabel: string | undefined): boolean;
|
|
21
22
|
export declare function thinkingChipVisible(snapshot: string): boolean;
|
|
@@ -522,24 +522,29 @@ export function snapshotHasUsableComposerControls(snapshot) {
|
|
|
522
522
|
return hasComposer && hasAddFiles;
|
|
523
523
|
}
|
|
524
524
|
|
|
525
|
+
/**
|
|
526
|
+
* @param {SnapshotEntry} entry
|
|
527
|
+
* @returns {boolean}
|
|
528
|
+
*/
|
|
529
|
+
export function matchesModelConfigurationOpener(entry) {
|
|
530
|
+
if (entry.disabled || entry.kind !== "button" || typeof entry.label !== "string") return false;
|
|
531
|
+
const label = normalizeChipLabel(entry.label);
|
|
532
|
+
return label === "Model"
|
|
533
|
+
|| label === "Model selector"
|
|
534
|
+
|| label === "Select ChatGPT model"
|
|
535
|
+
|| matchesCompactIntelligenceOpenerLabel(label)
|
|
536
|
+
|| BARE_EFFORT_PATTERN.test(label)
|
|
537
|
+
|| ["instant", "thinking", "pro"].some((family) => matchesModelFamilyLabel(label, /** @type {OracleUiModelFamily} */ (family)))
|
|
538
|
+
|| THINKING_CHIP_PATTERN.test(label)
|
|
539
|
+
|| PRO_CHIP_PATTERN.test(label);
|
|
540
|
+
}
|
|
541
|
+
|
|
525
542
|
/**
|
|
526
543
|
* @param {string} snapshot
|
|
527
544
|
* @returns {boolean}
|
|
528
545
|
*/
|
|
529
546
|
export function snapshotHasModelOpener(snapshot) {
|
|
530
|
-
|
|
531
|
-
const entries = parseSnapshotEntries(snapshot);
|
|
532
|
-
return entries.some((entry) => {
|
|
533
|
-
if (entry.disabled || entry.kind !== "button" || typeof entry.label !== "string") return false;
|
|
534
|
-
const label = normalizeChipLabel(entry.label);
|
|
535
|
-
return label === "Model"
|
|
536
|
-
|| label === "Model selector"
|
|
537
|
-
|| COMPACT_INTELLIGENCE_OPENER_PATTERN.test(label)
|
|
538
|
-
|| EFFORT_LABELS.has(label)
|
|
539
|
-
|| ["instant", "thinking", "pro"].some((family) => matchesModelFamilyLabel(label, /** @type {OracleUiModelFamily} */ (family)))
|
|
540
|
-
|| THINKING_CHIP_PATTERN.test(label)
|
|
541
|
-
|| PRO_CHIP_PATTERN.test(label);
|
|
542
|
-
});
|
|
547
|
+
return parseSnapshotEntries(snapshot).some(matchesModelConfigurationOpener);
|
|
543
548
|
}
|
|
544
549
|
|
|
545
550
|
/**
|
|
@@ -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 = {}) {
|
|
@@ -1781,7 +1770,7 @@ async function assistantMessages(job) {
|
|
|
1781
1770
|
job,
|
|
1782
1771
|
toJsonScript(`
|
|
1783
1772
|
const headings = Array.from(document.querySelectorAll('h1,h2,h3,h4,h5,h6,[role="heading"]'))
|
|
1784
|
-
.filter((el) => (el.textContent || '').trim() === 'ChatGPT said:');
|
|
1773
|
+
.filter((el) => el.getAttribute('data-conversation-role') === 'assistant' || (el.textContent || '').trim() === 'ChatGPT said:');
|
|
1785
1774
|
const renderText = (node) => {
|
|
1786
1775
|
if (!node) return '';
|
|
1787
1776
|
const clone = node.cloneNode(true);
|
|
@@ -1807,7 +1796,10 @@ async function assistantMessages(job) {
|
|
|
1807
1796
|
.trim();
|
|
1808
1797
|
return text;
|
|
1809
1798
|
};
|
|
1810
|
-
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
|
+
}));
|
|
1811
1803
|
const messageNodes = Array.from(document.querySelectorAll('[data-testid="assistant-message"], [data-message-author-role="assistant"]'));
|
|
1812
1804
|
const nodeMessages = messageNodes.map((node) => ({ text: renderText(node) }));
|
|
1813
1805
|
return {
|
|
@@ -1817,7 +1809,10 @@ async function assistantMessages(job) {
|
|
|
1817
1809
|
);
|
|
1818
1810
|
|
|
1819
1811
|
if (!Array.isArray(result?.messages)) return [];
|
|
1820
|
-
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
|
+
}));
|
|
1821
1816
|
}
|
|
1822
1817
|
|
|
1823
1818
|
async function grokAssistantMessages(job) {
|
|
@@ -1886,7 +1881,7 @@ async function waitForChatCompletion(job, baselineAssistantCount) {
|
|
|
1886
1881
|
while (Date.now() < timeoutAt) {
|
|
1887
1882
|
await heartbeat();
|
|
1888
1883
|
const [snapshot, body] = await Promise.all([snapshotText(job), pageText(job).catch(() => "")]);
|
|
1889
|
-
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"');
|
|
1890
1885
|
const hasRetryButton = snapshot.includes('button "Retry"');
|
|
1891
1886
|
const copyResponseCount = isGrokJob(job) ? (snapshot.match(/button "Copy"/g) || []).length : (snapshot.match(/Copy response/g) || []).length;
|
|
1892
1887
|
throwIfProviderTransientError(job, snapshot, "waiting for response completion");
|
|
@@ -1894,7 +1889,7 @@ async function waitForChatCompletion(job, baselineAssistantCount) {
|
|
|
1894
1889
|
const messages = await assistantMessages(job);
|
|
1895
1890
|
const targetMessage = messages[baselineAssistantCount];
|
|
1896
1891
|
const targetText = targetMessage?.text || "";
|
|
1897
|
-
const hasTargetCopyResponse = copyResponseCount > baselineAssistantCount;
|
|
1892
|
+
const hasTargetCopyResponse = copyResponseCount > baselineAssistantCount || targetMessage?.turnComplete === true;
|
|
1898
1893
|
|
|
1899
1894
|
if (!hasStopStreaming && hasRetryButton && responseFailureText) {
|
|
1900
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",
|
|
@@ -87,7 +87,7 @@
|
|
|
87
87
|
"typebox": "*"
|
|
88
88
|
},
|
|
89
89
|
"overrides": {
|
|
90
|
-
"basic-ftp": "6.2.
|
|
90
|
+
"basic-ftp": "6.2.3",
|
|
91
91
|
"protobufjs": "7.6.6"
|
|
92
92
|
},
|
|
93
93
|
"devDependencies": {
|
|
@@ -61,8 +61,25 @@ function currentGitHead() {
|
|
|
61
61
|
return git(["rev-parse", "HEAD"]);
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
-
function
|
|
65
|
-
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 };
|
|
66
83
|
}
|
|
67
84
|
|
|
68
85
|
function currentGitStatus() {
|
|
@@ -230,8 +247,6 @@ function validateProof(proof, path) {
|
|
|
230
247
|
const errors = [];
|
|
231
248
|
const { name, version } = packageMetadata();
|
|
232
249
|
const gitHead = currentGitHead();
|
|
233
|
-
const gitHeadCommittedAt = currentGitHeadCommittedAt();
|
|
234
|
-
const gitHeadCommittedAtMs = Date.parse(gitHeadCommittedAt);
|
|
235
250
|
const gitStatus = currentGitStatus();
|
|
236
251
|
const presetRegistry = canonicalPresets();
|
|
237
252
|
const requiredPresets = Object.keys(presetRegistry);
|
|
@@ -246,10 +261,17 @@ function validateProof(proof, path) {
|
|
|
246
261
|
return errors;
|
|
247
262
|
}
|
|
248
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
|
+
|
|
249
271
|
if (proof.schemaVersion !== 1) errors.push("schemaVersion must be 1");
|
|
250
272
|
if (proof.packageName !== name) errors.push(`packageName must be ${name}`);
|
|
251
|
-
if (proof.packageVersion !==
|
|
252
|
-
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`);
|
|
253
275
|
if (proof.provider !== "chatgpt") errors.push('provider must be "chatgpt"');
|
|
254
276
|
if (proof.extensionUnderTest !== "loaded-extension") errors.push('extensionUnderTest must be "loaded-extension"');
|
|
255
277
|
|
|
@@ -262,7 +284,7 @@ function validateProof(proof, path) {
|
|
|
262
284
|
const maxAgeMs = maxAgeHours() * 60 * 60 * 1000;
|
|
263
285
|
if (ageMs < 0) errors.push("validatedAt must not be in the future");
|
|
264
286
|
if (ageMs > maxAgeMs) errors.push(`validatedAt is older than ${maxAgeHours()} hours`);
|
|
265
|
-
if (proofValidatedAtMs <= gitHeadCommittedAtMs) errors.push("validatedAt must be after
|
|
287
|
+
if (proofValidatedAtMs <= gitHeadCommittedAtMs) errors.push("validatedAt must be after proven git commit time");
|
|
266
288
|
}
|
|
267
289
|
|
|
268
290
|
const jobs = proof.jobs;
|
|
@@ -277,8 +299,8 @@ function validateProof(proof, path) {
|
|
|
277
299
|
canonicalPreset: presetRegistry[preset],
|
|
278
300
|
proofJob: jobs[preset],
|
|
279
301
|
packageName: name,
|
|
280
|
-
packageVersion:
|
|
281
|
-
gitHead,
|
|
302
|
+
packageVersion: expectedVersion,
|
|
303
|
+
gitHead: expectedProofHead,
|
|
282
304
|
gitHeadCommittedAtMs,
|
|
283
305
|
proofValidatedAtMs,
|
|
284
306
|
errors,
|
|
@@ -15,20 +15,21 @@ const functions = names.flatMap((name) => {
|
|
|
15
15
|
return `${worker.slice(start - 6, start) === "async " ? "async " : ""}${worker.slice(start, end)}`;
|
|
16
16
|
}).join("\n");
|
|
17
17
|
|
|
18
|
-
function harness({ initial = 3, max = "4", wrongLabel = false, stuck = false, mountingReads = 0 } = {}) {
|
|
18
|
+
function harness({ initial = 3, max = "4", wrongLabel = false, stuck = false, mountingReads = 0, openerLabel } = {}) {
|
|
19
19
|
const labels = ["Instant", "Medium", "High", "Extra High", "Pro"];
|
|
20
20
|
let value = initial;
|
|
21
21
|
let open = false;
|
|
22
22
|
let verified = false;
|
|
23
|
+
let now = 0;
|
|
23
24
|
const snapshot = () => open
|
|
24
25
|
? '- button "Thinking effort" [expanded=true, ref=e1]\n- menu "Thinking effort" [ref=e2]\n- menuitem "Select model" [ref=e3]\n- menuitem "Power" [ref=e4]'
|
|
25
|
-
: `- button "${value === 4 ? "6 Pro" : labels[value]}" [expanded=false, ref=e1]\n- textbox "Ask ChatGPT" [ref=e5]\n- button "Add files and more" [ref=e6]`;
|
|
26
|
+
: `- button "${openerLabel ?? (value === 4 ? "6 Pro" : labels[value])}" [expanded=false, ref=e1]\n- textbox "Ask ChatGPT" [ref=e5]\n- button "Add files and more" [ref=e6]`;
|
|
26
27
|
const control = {
|
|
27
28
|
closest: () => null,
|
|
28
29
|
getAttribute: () => "description",
|
|
29
30
|
querySelector: () => mountingReads-- > 0 ? null : ({ getAttribute: (name) => ({ "aria-valuemin": "0", "aria-valuemax": max, "aria-valuenow": String(value) })[name] }),
|
|
30
31
|
};
|
|
31
|
-
const context = vm.createContext({ ...ui, parseSnapshotEntries, Date, Error, JSON, Number,
|
|
32
|
+
const context = vm.createContext({ ...ui, parseSnapshotEntries, Date: { now: () => now++ }, Error, JSON, Number,
|
|
32
33
|
CHATGPT_LABELS: { configure: "Configure..." },
|
|
33
34
|
MODEL_CONFIGURATION_OPEN_TIMEOUT_MS: 30, MODEL_CONFIGURATION_SETTLE_TIMEOUT_MS: 30,
|
|
34
35
|
MODEL_CONFIGURATION_CLOSE_RETRY_MS: 1, MODEL_CONFIGURATION_SETTLE_POLL_MS: 0,
|
|
@@ -58,19 +59,21 @@ function harness({ initial = 3, max = "4", wrongLabel = false, stuck = false, mo
|
|
|
58
59
|
}
|
|
59
60
|
|
|
60
61
|
test("worker configures every preset through the current Power picker and closes it", async () => {
|
|
61
|
-
for (const [
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
62
|
+
for (const openerLabel of [undefined, "Select ChatGPT model"]) {
|
|
63
|
+
for (const [selection, expected] of [
|
|
64
|
+
[{ modelFamily: "instant", autoSwitchToThinking: false }, 0],
|
|
65
|
+
[{ modelFamily: "instant", autoSwitchToThinking: true }, 0],
|
|
66
|
+
[{ modelFamily: "thinking", effort: "light" }, 1],
|
|
67
|
+
[{ modelFamily: "thinking", effort: "standard" }, 1],
|
|
68
|
+
[{ modelFamily: "thinking", effort: "extended" }, 2],
|
|
69
|
+
[{ modelFamily: "thinking", effort: "heavy" }, 3],
|
|
70
|
+
[{ modelFamily: "pro", effort: "standard" }, 4],
|
|
71
|
+
[{ modelFamily: "pro", effort: "extended" }, 4],
|
|
72
|
+
]) {
|
|
73
|
+
const h = harness({ initial: expected === 4 ? 0 : 4, mountingReads: 2, openerLabel });
|
|
74
|
+
await h.configure(selection);
|
|
75
|
+
assert.deepEqual(h.state, { value: expected, open: false, verified: true });
|
|
76
|
+
}
|
|
74
77
|
}
|
|
75
78
|
});
|
|
76
79
|
|
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
// Default mode is packed-install release proof. Source mode is inner-loop/debug only.
|
|
4
4
|
|
|
5
5
|
import { spawn } from "node:child_process";
|
|
6
|
-
import {
|
|
6
|
+
import { createHash } from "node:crypto";
|
|
7
|
+
import { copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
|
|
7
8
|
import { mkdtempSync } from "node:fs";
|
|
8
9
|
import { tmpdir } from "node:os";
|
|
9
10
|
import { basename, dirname, join, resolve } from "node:path";
|
|
@@ -25,6 +26,8 @@ Modes:
|
|
|
25
26
|
Environment:
|
|
26
27
|
PI_HOST_CLI selected Pi manifest bin entry, invoked with this Node executable (default: pi on PATH)
|
|
27
28
|
PI_COMPAT_EXPECTED_VERSION expected Pi version (default: package development baseline)
|
|
29
|
+
PI_ORACLE_RELEASE_TARBALL retained tarball to install instead of the newly packed archive
|
|
30
|
+
PI_ORACLE_RELEASE_SHA256 expected retained tarball SHA-256, when supplied
|
|
28
31
|
PI_ORACLE_REAL_TEST_PROVIDER pi provider for the test agent (default: ${DEFAULT_PROVIDER})
|
|
29
32
|
PI_ORACLE_REAL_TEST_MODEL pi model for the test agent (default: ${DEFAULT_MODEL})
|
|
30
33
|
PI_ORACLE_REAL_TEST_TIMEOUT_MS per-agent timeout in ms (default: ${DEFAULT_TIMEOUT_MS})
|
|
@@ -262,8 +265,16 @@ async function preparePackedProject({ runDir, provider, model, timeoutMs }) {
|
|
|
262
265
|
timeoutMs: 120_000,
|
|
263
266
|
});
|
|
264
267
|
const tarballName = pack.stdout.split(/\r?\n/).map((line) => line.trim()).filter(Boolean).at(-1);
|
|
265
|
-
|
|
268
|
+
let tarballPath = tarballName ? join(packDir, tarballName) : undefined;
|
|
266
269
|
if (!tarballPath || !existsSync(tarballPath)) throw new Error(`npm pack did not produce a tarball in ${packDir}`);
|
|
270
|
+
if (env("PI_ORACLE_RELEASE_TARBALL")) {
|
|
271
|
+
tarballPath = join(packDir, "release-candidate.tgz");
|
|
272
|
+
copyFileSync(resolve(env("PI_ORACLE_RELEASE_TARBALL")), tarballPath);
|
|
273
|
+
const hash = createHash("sha256").update(readFileSync(tarballPath)).digest("hex");
|
|
274
|
+
const expected = env("PI_ORACLE_RELEASE_SHA256");
|
|
275
|
+
if (expected && hash !== expected) throw new Error("Retained release candidate checksum changed");
|
|
276
|
+
console.log(`ORACLE_RELEASE_CANDIDATE_SHA256=${hash}`);
|
|
277
|
+
}
|
|
267
278
|
writeFileSync(join(installDir, "packed-tarball.txt"), `${tarballPath}\n`);
|
|
268
279
|
|
|
269
280
|
await mustRun(installDir, "npm-init", npm, ["init", "-y"], { cwd: piProject, env: process.env, timeoutMs: 60_000 });
|
|
@@ -4,6 +4,6 @@ USER root
|
|
|
4
4
|
RUN apt-get update \
|
|
5
5
|
&& apt-get install -y --no-install-recommends zstd rsync \
|
|
6
6
|
&& rm -rf /var/lib/apt/lists/*
|
|
7
|
-
RUN npm install -g npm
|
|
8
|
-
&& npm install -g agent-browser
|
|
7
|
+
RUN npm install -g npm \
|
|
8
|
+
&& npm install -g --allow-scripts=agent-browser agent-browser
|
|
9
9
|
USER circleci
|
|
@@ -58,6 +58,7 @@ export function buildTargetBaseArgs(targetName, config = {}) {
|
|
|
58
58
|
switch (targetName) {
|
|
59
59
|
case "macos": {
|
|
60
60
|
const host = env("PI_ORACLE_SMOKE_MAC_HOST") || env("PLATFORM_SMOKE_MAC_HOST") || "localhost";
|
|
61
|
+
const port = env("PI_ORACLE_SMOKE_MAC_PORT") || env("PLATFORM_SMOKE_MAC_PORT") || "22";
|
|
61
62
|
const user = env("PI_ORACLE_SMOKE_MAC_USER") || env("PLATFORM_SMOKE_MAC_USER") || env("USER");
|
|
62
63
|
const workRoot = env("PI_ORACLE_SMOKE_MAC_WORK_ROOT") || env("PLATFORM_SMOKE_MAC_WORK_ROOT") || `/Users/${env("USER")}/crabbox/${config.packageName ?? "pi-oracle"}`;
|
|
63
64
|
return [
|
|
@@ -65,7 +66,7 @@ export function buildTargetBaseArgs(targetName, config = {}) {
|
|
|
65
66
|
"--target", "macos",
|
|
66
67
|
"--static-host", host,
|
|
67
68
|
"--static-user", user,
|
|
68
|
-
"--static-port",
|
|
69
|
+
"--static-port", port,
|
|
69
70
|
"--static-work-root", workRoot,
|
|
70
71
|
];
|
|
71
72
|
}
|
|
@@ -82,6 +83,19 @@ export function buildTargetBaseArgs(targetName, config = {}) {
|
|
|
82
83
|
const snapshot = env("PI_ORACLE_SMOKE_WINDOWS_SNAPSHOT") || env("PLATFORM_SMOKE_WINDOWS_SNAPSHOT") || config.windowsParallels?.snapshot || "crabbox-ready";
|
|
83
84
|
const user = env("PI_ORACLE_SMOKE_WINDOWS_USER") || env("PLATFORM_SMOKE_WINDOWS_USER") || env("USER");
|
|
84
85
|
const workRoot = env("PI_ORACLE_SMOKE_WINDOWS_NATIVE_WORK_ROOT") || env("PLATFORM_SMOKE_WINDOWS_WORK_ROOT") || `C:\\crabbox\\${config.packageName ?? "pi-oracle"}`;
|
|
86
|
+
const host = env("PI_ORACLE_SMOKE_WINDOWS_HOST") || env("PLATFORM_SMOKE_WINDOWS_HOST");
|
|
87
|
+
if (host) {
|
|
88
|
+
const port = env("PI_ORACLE_SMOKE_WINDOWS_PORT") || env("PLATFORM_SMOKE_WINDOWS_PORT") || "22";
|
|
89
|
+
return [
|
|
90
|
+
"--provider", "ssh",
|
|
91
|
+
"--target", "windows",
|
|
92
|
+
"--windows-mode", "normal",
|
|
93
|
+
"--static-host", host,
|
|
94
|
+
"--static-user", user,
|
|
95
|
+
"--static-port", port,
|
|
96
|
+
"--static-work-root", workRoot,
|
|
97
|
+
];
|
|
98
|
+
}
|
|
85
99
|
return [
|
|
86
100
|
"--provider", "parallels",
|
|
87
101
|
"--target", "windows",
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// Preflight checks for the pi-oracle Crabbox platform smoke gate.
|
|
2
2
|
|
|
3
3
|
import { execFileSync, execSync } from "node:child_process";
|
|
4
|
-
import { accessSync, constants,
|
|
5
|
-
import { resolve } from "node:path";
|
|
4
|
+
import { accessSync, constants, mkdirSync, readdirSync, unlinkSync, writeFileSync } from "node:fs";
|
|
5
|
+
import { join, relative, resolve } from "node:path";
|
|
6
6
|
import { buildTargetBaseArgs } from "./crabbox-runner.mjs";
|
|
7
7
|
|
|
8
8
|
let failures = 0;
|
|
@@ -131,28 +131,53 @@ function verifyPackageExclusions() {
|
|
|
131
131
|
forbidden.length ? fail(`forbidden files in package: ${forbidden.join(", ")}`) : ok("package excludes local env/artifact/debug state");
|
|
132
132
|
}
|
|
133
133
|
|
|
134
|
+
function findForbiddenFiles(dir, depth = 0) {
|
|
135
|
+
return readdirSync(dir, { withFileTypes: true }).flatMap((entry) => {
|
|
136
|
+
const path = join(dir, entry.name);
|
|
137
|
+
const matches = entry.name === ".env" || entry.name.startsWith(".env.") || entry.name.endsWith(".tgz");
|
|
138
|
+
return [
|
|
139
|
+
...(matches ? [relative(process.cwd(), path)] : []),
|
|
140
|
+
...(entry.isDirectory() && depth < 2 ? findForbiddenFiles(path, depth + 1) : []),
|
|
141
|
+
];
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
134
145
|
function verifyRepoForbiddenFiles() {
|
|
135
|
-
const forbidden =
|
|
136
|
-
forbidden ? fail(`forbidden local secret/package artifact(s): ${forbidden}`) : ok("no .env/.env.* or package tarballs at repo root depth");
|
|
146
|
+
const forbidden = findForbiddenFiles(process.cwd());
|
|
147
|
+
forbidden.length ? fail(`forbidden local secret/package artifact(s): ${forbidden.join(", ")}`) : ok("no .env/.env.* or package tarballs at repo root depth");
|
|
137
148
|
for (const dir of [".artifacts", ".crabbox", ".debug", ".platform-smoke-runs"]) {
|
|
138
|
-
const ignored =
|
|
149
|
+
const ignored = silent("git", ["check-ignore", "-q", `${dir}/probe`]) !== null;
|
|
139
150
|
ignored ? ok(`${dir}/ is gitignored`) : fail(`${dir}/ must be gitignored`);
|
|
140
151
|
}
|
|
141
|
-
const trackedForbidden =
|
|
152
|
+
const trackedForbidden = silent("git", ["ls-files", ".env*", "*.tgz", ".artifacts/*", ".crabbox/*", ".debug/*", ".platform-smoke-runs/*"]);
|
|
142
153
|
trackedForbidden ? fail(`forbidden tracked local state: ${trackedForbidden}`) : ok("no forbidden local state is tracked");
|
|
143
154
|
verifyPackageExclusions();
|
|
144
155
|
}
|
|
145
156
|
|
|
146
157
|
export async function runDoctor(config) {
|
|
147
158
|
failures = 0;
|
|
159
|
+
const targets = config.requiredTargets ?? [];
|
|
160
|
+
const hasMac = targets.includes("macos");
|
|
161
|
+
const hasUbuntu = targets.includes("ubuntu");
|
|
162
|
+
const hasWindows = targets.includes("windows-native");
|
|
163
|
+
const windowsArgs = hasWindows ? buildTargetBaseArgs("windows-native", config) : [];
|
|
164
|
+
const windowsStatic = windowsArgs.includes("--static-host");
|
|
148
165
|
console.log("\n── Environment ──");
|
|
149
166
|
const cbox = env("PI_ORACLE_SMOKE_CRABBOX") || env("PLATFORM_SMOKE_CRABBOX") || "crabbox";
|
|
150
167
|
ok(`Crabbox binary = ${resolveCommand(cbox) ?? cbox}${env("PI_ORACLE_SMOKE_CRABBOX") || env("PLATFORM_SMOKE_CRABBOX") ? " (env override)" : " (PATH)"}`);
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
168
|
+
if (hasMac) {
|
|
169
|
+
ok(`PI_ORACLE_SMOKE_MAC_HOST = ${env("PI_ORACLE_SMOKE_MAC_HOST") || env("PLATFORM_SMOKE_MAC_HOST") || "localhost"}`);
|
|
170
|
+
ok(`PI_ORACLE_SMOKE_MAC_PORT = ${env("PI_ORACLE_SMOKE_MAC_PORT") || env("PLATFORM_SMOKE_MAC_PORT") || "22"}`);
|
|
171
|
+
ok(`PI_ORACLE_SMOKE_MAC_USER = ${env("PI_ORACLE_SMOKE_MAC_USER") || env("PLATFORM_SMOKE_MAC_USER") || env("USER")}`);
|
|
172
|
+
}
|
|
173
|
+
if (hasUbuntu) ok(`PI_ORACLE_SMOKE_UBUNTU_IMAGE = ${env("PI_ORACLE_SMOKE_UBUNTU_IMAGE") || env("PLATFORM_SMOKE_UBUNTU_IMAGE") || config.ubuntuContainerImage || "pi-oracle-platform-smoke:node24"}`);
|
|
174
|
+
if (hasWindows && windowsStatic) {
|
|
175
|
+
ok(`Windows SSH host = ${windowsArgs[windowsArgs.indexOf("--static-host") + 1]}`);
|
|
176
|
+
ok(`Windows SSH port = ${windowsArgs[windowsArgs.indexOf("--static-port") + 1]}`);
|
|
177
|
+
} else if (hasWindows) {
|
|
178
|
+
ok(`PI_ORACLE_SMOKE_WINDOWS_VM = ${windowsVmName(config)}`);
|
|
179
|
+
ok(`PI_ORACLE_SMOKE_WINDOWS_SNAPSHOT = ${windowsSnapshotName(config)}`);
|
|
180
|
+
}
|
|
156
181
|
|
|
157
182
|
console.log("\n── Crabbox ──");
|
|
158
183
|
const resolvedCbox = resolveCommand(cbox);
|
|
@@ -166,15 +191,22 @@ export async function runDoctor(config) {
|
|
|
166
191
|
compareVersions(versionLine, requiredVersion) >= 0 ? ok(`required version: ${requiredVersion}+`) : fail(`Crabbox version mismatch: need ${requiredVersion}+, got ${versionLine}`);
|
|
167
192
|
}
|
|
168
193
|
const providers = silent(cbox, ["providers"], { timeout: 30_000 }) ?? "";
|
|
169
|
-
|
|
194
|
+
const neededProviders = new Set([
|
|
195
|
+
...(hasMac || (hasWindows && windowsStatic) ? ["ssh"] : []),
|
|
196
|
+
...(hasUbuntu ? ["local-container"] : []),
|
|
197
|
+
...(hasWindows && !windowsStatic ? ["parallels"] : []),
|
|
198
|
+
]);
|
|
199
|
+
for (const provider of neededProviders) {
|
|
170
200
|
new RegExp(`^${provider}$`, "m").test(providers) ? ok(`provider available: ${provider}`) : fail(`crabbox providers missing ${provider}`);
|
|
171
201
|
}
|
|
172
|
-
runCrabboxDoctor(cbox, "macOS static SSH", buildTargetBaseArgs("macos", config), 120_000);
|
|
173
|
-
runCrabboxDoctor(cbox, "Windows native
|
|
174
|
-
runCrabboxDoctor(cbox, "local-container", buildTargetBaseArgs("ubuntu", config), 120_000);
|
|
202
|
+
if (hasMac) runCrabboxDoctor(cbox, "macOS static SSH", buildTargetBaseArgs("macos", config), 120_000);
|
|
203
|
+
if (hasWindows) runCrabboxDoctor(cbox, windowsStatic ? "Windows native SSH" : "Windows native Parallels", windowsArgs, 180_000);
|
|
204
|
+
if (hasUbuntu) runCrabboxDoctor(cbox, "local-container", buildTargetBaseArgs("ubuntu", config), 120_000);
|
|
175
205
|
|
|
176
206
|
console.log("\n── Host tools ──");
|
|
177
|
-
|
|
207
|
+
const hostTools = [["Node", "node --version"], ["npm", "npm --version"], ["git", "git --version"], ["tar", "tar --version"], ["rsync", "rsync --version"]];
|
|
208
|
+
if (hasUbuntu) hostTools.unshift(["Docker", "docker info --format '{{.ServerVersion}}'"]);
|
|
209
|
+
for (const [name, command] of hostTools) {
|
|
178
210
|
const output = shell(command);
|
|
179
211
|
output ? ok(`${name}: ${output.split("\n")[0]}`) : fail(`${name} not found or unavailable`);
|
|
180
212
|
}
|
|
@@ -183,7 +215,7 @@ export async function runDoctor(config) {
|
|
|
183
215
|
const major = Number(nodeVersion.replace(/^v/, "").split(".")[0]);
|
|
184
216
|
if (major < (config.nodeValidationMajor ?? 24)) fail(`Node ${nodeVersion}; need ${config.nodeValidationMajor ?? 24}+ for smoke validation`);
|
|
185
217
|
}
|
|
186
|
-
verifyWindowsTemplate(config);
|
|
218
|
+
if (hasWindows && !windowsStatic) verifyWindowsTemplate(config);
|
|
187
219
|
|
|
188
220
|
console.log("\n── Auth environment ──");
|
|
189
221
|
const requiredAuth = requiredRealSmokeAuthEnv(config);
|
|
@@ -193,7 +225,7 @@ export async function runDoctor(config) {
|
|
|
193
225
|
}
|
|
194
226
|
|
|
195
227
|
console.log("\n── Target tools ──");
|
|
196
|
-
for (const target of
|
|
228
|
+
for (const target of targets) runTargetToolProbe(cbox, target, config);
|
|
197
229
|
|
|
198
230
|
console.log("\n── Artifact root ──");
|
|
199
231
|
const artifactRoot = resolve(process.cwd(), config.artifactRoot ?? ".artifacts/platform-smoke");
|
|
@@ -217,8 +249,8 @@ export async function runDoctor(config) {
|
|
|
217
249
|
console.log(`\n=== Results: ${failures} failure(s) ===`);
|
|
218
250
|
if (failures > 0) {
|
|
219
251
|
process.exitCode = 1;
|
|
220
|
-
console.log("Fix the failures above before running
|
|
252
|
+
console.log("Fix the failures above before running the selected platform suites.");
|
|
221
253
|
} else {
|
|
222
|
-
console.log(
|
|
254
|
+
console.log(`Ready for platform suites on ${targets.join(", ")}.`);
|
|
223
255
|
}
|
|
224
256
|
}
|
|
@@ -28,17 +28,23 @@ function testHelpTextIncludesTargetsAndExamples() {
|
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
function testTargetSelection() {
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
31
|
+
for (const command of ["doctor", "run"]) {
|
|
32
|
+
const result = withEnv({ PI_ORACLE_SMOKE_CRABBOX: "/nonexistent/crabbox" }, () =>
|
|
33
|
+
runNode(["scripts/platform-smoke.mjs", command, "--target", "not-a-target"]));
|
|
34
|
+
assert.notEqual(result.status, 0, `${command}: unsupported targets should fail before Crabbox runs`);
|
|
35
|
+
assert.ifError(result.error);
|
|
36
|
+
assert.match(`${result.stdout ?? ""}\n${result.stderr ?? ""}`, /unsupported target: not-a-target/);
|
|
37
|
+
}
|
|
35
38
|
}
|
|
36
39
|
|
|
37
40
|
function testEmptyTargetSelectionRejected() {
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
41
|
+
for (const command of ["doctor", "run"]) {
|
|
42
|
+
const result = withEnv({ PI_ORACLE_SMOKE_CRABBOX: "/nonexistent/crabbox" }, () =>
|
|
43
|
+
runNode(["scripts/platform-smoke.mjs", command, "--target", ",,"]));
|
|
44
|
+
assert.notEqual(result.status, 0, `${command}: an empty target selection must fail instead of running nothing`);
|
|
45
|
+
assert.ifError(result.error);
|
|
46
|
+
assert.match(`${result.stdout ?? ""}\n${result.stderr ?? ""}`, /--target did not resolve to any targets/);
|
|
47
|
+
}
|
|
42
48
|
}
|
|
43
49
|
|
|
44
50
|
function testPackedInstallCommandRendering() {
|
|
@@ -93,6 +99,8 @@ function testTargetBaseArgsEnvPrecedence() {
|
|
|
93
99
|
PLATFORM_SMOKE_UBUNTU_IMAGE: undefined,
|
|
94
100
|
PI_ORACLE_SMOKE_MAC_WORK_ROOT: undefined,
|
|
95
101
|
PLATFORM_SMOKE_MAC_WORK_ROOT: undefined,
|
|
102
|
+
PI_ORACLE_SMOKE_MAC_PORT: undefined,
|
|
103
|
+
PLATFORM_SMOKE_MAC_PORT: undefined,
|
|
96
104
|
PI_ORACLE_SMOKE_WINDOWS_NATIVE_WORK_ROOT: undefined,
|
|
97
105
|
PLATFORM_SMOKE_WINDOWS_WORK_ROOT: undefined,
|
|
98
106
|
}, () => {
|
|
@@ -122,6 +130,14 @@ function testTargetBaseArgsEnvPrecedence() {
|
|
|
122
130
|
args = buildTargetBaseArgs("macos", {});
|
|
123
131
|
assert(args.includes("/invariants/private-work") && !args.includes("/invariants/shared-work"), "private PI_ORACLE_SMOKE_MAC_WORK_ROOT alias should win over the shared alias");
|
|
124
132
|
|
|
133
|
+
assert.equal(args[args.indexOf("--static-port") + 1], "22", "macOS SSH defaults to port 22");
|
|
134
|
+
process.env.PLATFORM_SMOKE_MAC_PORT = "2200";
|
|
135
|
+
args = buildTargetBaseArgs("macos", {});
|
|
136
|
+
assert.equal(args[args.indexOf("--static-port") + 1], "2200", "shared macOS port reaches Crabbox");
|
|
137
|
+
process.env.PI_ORACLE_SMOKE_MAC_PORT = "2201";
|
|
138
|
+
args = buildTargetBaseArgs("macos", {});
|
|
139
|
+
assert.equal(args[args.indexOf("--static-port") + 1], "2201", "project macOS port takes precedence");
|
|
140
|
+
|
|
125
141
|
process.env.PLATFORM_SMOKE_WINDOWS_WORK_ROOT = "C:\\invariants\\shared-work";
|
|
126
142
|
args = buildTargetBaseArgs("windows-native", {});
|
|
127
143
|
assert(args.includes("C:\\invariants\\shared-work"), "shared PLATFORM_SMOKE_WINDOWS_WORK_ROOT override should reach target args");
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
param(
|
|
2
2
|
[string]$PackageName = "pi-oracle",
|
|
3
|
-
[int]$NodeValidationMajor = 24
|
|
3
|
+
[int]$NodeValidationMajor = 24,
|
|
4
|
+
[string]$CandidatePath = "",
|
|
5
|
+
[string]$CandidateSha256 = ""
|
|
4
6
|
)
|
|
5
7
|
|
|
6
8
|
$ErrorActionPreference = "Continue"
|
|
@@ -77,6 +79,20 @@ Write-Output "PLATFORM_NPM_PACK_EXIT=$PACK_EXIT"
|
|
|
77
79
|
if ($PackTarball -and (Test-Path -LiteralPath $PackTarball)) {
|
|
78
80
|
Move-Item -LiteralPath $PackTarball -Destination (Join-Path $PackDir $PackTarball) -Force
|
|
79
81
|
}
|
|
82
|
+
if ($CandidatePath) {
|
|
83
|
+
if (-not (Test-Path -LiteralPath $CandidatePath)) {
|
|
84
|
+
Write-Output "Retained release candidate is missing"
|
|
85
|
+
exit 1
|
|
86
|
+
}
|
|
87
|
+
$CandidateHash = (Get-FileHash -LiteralPath $CandidatePath -Algorithm SHA256).Hash.ToLowerInvariant()
|
|
88
|
+
Write-Output "PLATFORM_CANDIDATE_SHA256=$CandidateHash"
|
|
89
|
+
if ($CandidateHash -ne $CandidateSha256.ToLowerInvariant()) {
|
|
90
|
+
Write-Output "Retained release candidate checksum changed"
|
|
91
|
+
exit 1
|
|
92
|
+
}
|
|
93
|
+
Copy-Item -LiteralPath $CandidatePath -Destination (Join-Path $PackDir "release-candidate.tgz") -ErrorAction Stop
|
|
94
|
+
$PackTarball = "release-candidate.tgz"
|
|
95
|
+
}
|
|
80
96
|
Write-Output "PLATFORM_PACKED_TARBALL=$PackTarball"
|
|
81
97
|
Set-Content -Path (Join-Path $PackDir "packed-tarball.txt") -Value $PackTarball
|
|
82
98
|
|
|
@@ -95,6 +95,10 @@ export function buildRealExtensionCommand(targetName = "ubuntu", config = {}) {
|
|
|
95
95
|
`$env:PI_ORACLE_REAL_TEST_PROVIDER = ${powershellSingleQuote(provider)}`,
|
|
96
96
|
`$env:PI_ORACLE_REAL_TEST_MODEL = ${powershellSingleQuote(model)}`,
|
|
97
97
|
`$env:PI_ORACLE_REAL_TEST_MODEL_AGENT = ${powershellSingleQuote(useModelAgent ? "1" : "")}`,
|
|
98
|
+
...(config.releaseCandidate ? [
|
|
99
|
+
`$env:PI_ORACLE_RELEASE_TARBALL = Join-Path (Get-Location) ${powershellSingleQuote(config.releaseCandidate.path)}`,
|
|
100
|
+
`$env:PI_ORACLE_RELEASE_SHA256 = ${powershellSingleQuote(config.releaseCandidate.sha256)}`,
|
|
101
|
+
] : []),
|
|
98
102
|
"$env:PI_ORACLE_REAL_TEST_TIMEOUT_MS = '360000'",
|
|
99
103
|
"if (-not (Test-Path -LiteralPath 'node_modules')) { npm.cmd ci; if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } }",
|
|
100
104
|
"npm.cmd run smoke:real:doctor",
|
|
@@ -108,15 +112,22 @@ export function buildRealExtensionCommand(targetName = "ubuntu", config = {}) {
|
|
|
108
112
|
`export PI_ORACLE_REAL_TEST_PROVIDER=${shellQuote(provider)}`,
|
|
109
113
|
`export PI_ORACLE_REAL_TEST_MODEL=${shellQuote(model)}`,
|
|
110
114
|
`export PI_ORACLE_REAL_TEST_MODEL_AGENT=${shellQuote(useModelAgent ? "1" : "")}`,
|
|
115
|
+
...(config.releaseCandidate ? [
|
|
116
|
+
`export PI_ORACLE_RELEASE_TARBALL="$PWD"/${shellQuote(config.releaseCandidate.path)}`,
|
|
117
|
+
`export PI_ORACLE_RELEASE_SHA256=${shellQuote(config.releaseCandidate.sha256)}`,
|
|
118
|
+
] : []),
|
|
111
119
|
'if [ ! -d node_modules ]; then npm ci; fi',
|
|
112
120
|
"npm run smoke:real:doctor",
|
|
113
121
|
"npm run smoke:real:packed",
|
|
114
122
|
].join("\n");
|
|
115
123
|
}
|
|
116
124
|
|
|
117
|
-
export function buildPlatformBuildCommand(targetName = "ubuntu", packageName = "pi-oracle", nodeValidationMajor = 24) {
|
|
125
|
+
export function buildPlatformBuildCommand(targetName = "ubuntu", packageName = "pi-oracle", nodeValidationMajor = 24, releaseCandidate) {
|
|
118
126
|
if (targetName === "windows-native") {
|
|
119
|
-
|
|
127
|
+
const candidate = releaseCandidate
|
|
128
|
+
? ` -CandidatePath ${powershellSingleQuote(releaseCandidate.path)} -CandidateSha256 ${powershellSingleQuote(releaseCandidate.sha256)}`
|
|
129
|
+
: "";
|
|
130
|
+
return `powershell.exe -NoLogo -NoProfile -ExecutionPolicy Bypass -File .\\scripts\\platform-smoke\\platform-build-windows.ps1 -PackageName ${packageName} -NodeValidationMajor ${nodeValidationMajor}${candidate}`;
|
|
120
131
|
}
|
|
121
132
|
const lines = [];
|
|
122
133
|
lines.push("set -o pipefail");
|
|
@@ -172,6 +183,12 @@ export function buildPlatformBuildCommand(targetName = "ubuntu", packageName = "
|
|
|
172
183
|
lines.push('cat "$PACK_DIR/npm-pack.stderr.txt"');
|
|
173
184
|
lines.push('echo "PLATFORM_NPM_PACK_EXIT=$PACK_EXIT"');
|
|
174
185
|
lines.push('if [ -n "$PACK_TARBALL" ] && [ -f "$PACK_TARBALL" ]; then mv "$PACK_TARBALL" "$PACK_DIR/$PACK_TARBALL"; fi');
|
|
186
|
+
if (releaseCandidate) {
|
|
187
|
+
lines.push(`CANDIDATE="$SOURCE_ROOT"/${shellQuote(releaseCandidate.path)}`);
|
|
188
|
+
lines.push('CANDIDATE_SHA256=$(node -e \'const {createHash}=require("node:crypto");const {readFileSync}=require("node:fs");console.log(createHash("sha256").update(readFileSync(process.argv[1])).digest("hex"))\' "$CANDIDATE")');
|
|
189
|
+
lines.push('echo "PLATFORM_CANDIDATE_SHA256=$CANDIDATE_SHA256"');
|
|
190
|
+
lines.push(`if [ "$CANDIDATE_SHA256" = ${shellQuote(releaseCandidate.sha256)} ]; then cp "$CANDIDATE" "$PACK_DIR/release-candidate.tgz" || exit 1; PACK_TARBALL=release-candidate.tgz; else echo "Retained release candidate checksum changed"; exit 1; fi`);
|
|
191
|
+
}
|
|
175
192
|
lines.push('echo "PLATFORM_PACKED_TARBALL=$PACK_TARBALL"');
|
|
176
193
|
lines.push('printf "%s\\n" "$PACK_TARBALL" > "$PACK_DIR/packed-tarball.txt"');
|
|
177
194
|
lines.push("");
|
|
@@ -213,7 +230,7 @@ export async function runTargetSuite(config, targetName, suiteName, leaseSession
|
|
|
213
230
|
writeFileSync(resolve(suiteDir, "suite.json"), JSON.stringify({ suiteName, writtenAt: new Date().toISOString() }, null, 2));
|
|
214
231
|
|
|
215
232
|
const command = suiteName === "platform-build"
|
|
216
|
-
? buildPlatformBuildCommand(targetName, config.packageName ?? "pi-oracle", config.nodeValidationMajor ?? 24)
|
|
233
|
+
? buildPlatformBuildCommand(targetName, config.packageName ?? "pi-oracle", config.nodeValidationMajor ?? 24, config.releaseCandidate)
|
|
217
234
|
: buildRealExtensionCommand(targetName, config);
|
|
218
235
|
writeCommand(suiteDir, command);
|
|
219
236
|
|
|
@@ -269,6 +286,8 @@ export async function runTargetSuite(config, targetName, suiteName, leaseSession
|
|
|
269
286
|
{ id: "platform-dependencies", fn: () => /PLATFORM_DEPS_EXIT=0/.test(stdout) },
|
|
270
287
|
{ id: "platform-verification", fn: () => /PLATFORM_NPM_TEST_EXIT=0/.test(stdout) },
|
|
271
288
|
{ id: "npm-pack", fn: () => /PLATFORM_NPM_PACK_EXIT=0/.test(stdout) && /PLATFORM_PACKED_TARBALL=\S+/.test(stdout) },
|
|
289
|
+
...(config.releaseCandidate ? [{ id: "release-candidate-sha256",
|
|
290
|
+
fn: () => markerValue(stdout, "PLATFORM_CANDIDATE_SHA256") === config.releaseCandidate.sha256 }] : []),
|
|
272
291
|
{ id: "packed-node-install", fn: () => /PLATFORM_PACKED_NODE_INSTALL_EXIT=0/.test(stdout) },
|
|
273
292
|
{ id: "pi-install", fn: () => /PLATFORM_PI_INSTALL_EXIT=0/.test(stdout) },
|
|
274
293
|
{ id: "pi-list", fn: () => /PLATFORM_PI_LIST_EXIT=0/.test(stdout) && listOutput.includes(packageName) && packageInstallPattern.test(listOutput) },
|
|
@@ -289,6 +308,8 @@ export async function runTargetSuite(config, targetName, suiteName, leaseSession
|
|
|
289
308
|
{ id: "real-smoke-doctor", fn: () => stdout.includes("Oracle real smoke doctor") && stdout.includes(`provider: ${provider}`) },
|
|
290
309
|
{ id: "real-smoke-marker", fn: () => stdout.includes("Oracle real smoke passed:") },
|
|
291
310
|
{ id: "real-smoke-packed-install", fn: () => stdout.includes("mode=packed") && stdout.includes("extension=./node_modules/pi-oracle") },
|
|
311
|
+
...(config.releaseCandidate ? [{ id: "release-candidate-sha256",
|
|
312
|
+
fn: () => markerValue(stdout, "ORACLE_RELEASE_CANDIDATE_SHA256") === config.releaseCandidate.sha256 }] : []),
|
|
292
313
|
{ id: "real-smoke-no-source-extension", fn: () => !command.includes("extensions/oracle/index.ts") && !/\bpi\s+(?:-e|--extension)\s+extensions\/oracle/.test(command) },
|
|
293
314
|
{ id: "no-secrets", fn: () => violations.length === 0, error: "redaction violations found" },
|
|
294
315
|
];
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
4
|
+
import { copyFileSync, mkdtempSync, readFileSync, rmSync } from "node:fs";
|
|
3
5
|
import { createRequire } from "node:module";
|
|
4
|
-
import { dirname, resolve } from "node:path";
|
|
6
|
+
import { dirname, relative, resolve, sep } from "node:path";
|
|
5
7
|
import { fileURLToPath } from "node:url";
|
|
6
8
|
|
|
7
9
|
const __filename = fileURLToPath(import.meta.url);
|
|
@@ -21,7 +23,7 @@ function printHelp() {
|
|
|
21
23
|
console.log(`Usage: node scripts/platform-smoke.mjs <command> [options]
|
|
22
24
|
|
|
23
25
|
Commands:
|
|
24
|
-
doctor
|
|
26
|
+
doctor [--target <names>] Run preflight checks for required or selected targets
|
|
25
27
|
run --target <names> Run one or more comma-separated targets concurrently
|
|
26
28
|
run --suite <name> Run one suite on the configured target(s)
|
|
27
29
|
|
|
@@ -48,14 +50,19 @@ Environment:
|
|
|
48
50
|
PLATFORM_SMOKE_CRABBOX Reusable Crabbox binary override (defaults to PATH)
|
|
49
51
|
PI_ORACLE_SMOKE_CRABBOX Project-specific Crabbox binary override (wins over PLATFORM_SMOKE_CRABBOX)
|
|
50
52
|
PLATFORM_SMOKE_MAC_HOST macOS SSH host (default: localhost)
|
|
53
|
+
PLATFORM_SMOKE_MAC_PORT macOS SSH port (default: 22)
|
|
51
54
|
PLATFORM_SMOKE_MAC_USER macOS SSH user (default: $USER)
|
|
52
55
|
PLATFORM_SMOKE_MAC_WORK_ROOT macOS Crabbox work root
|
|
53
56
|
PLATFORM_SMOKE_UBUNTU_IMAGE Optional local-container image override
|
|
54
57
|
PLATFORM_SMOKE_WINDOWS_VM Parallels source VM (default from config)
|
|
55
58
|
PLATFORM_SMOKE_WINDOWS_SNAPSHOT Parallels snapshot (default from config)
|
|
59
|
+
PLATFORM_SMOKE_WINDOWS_HOST Use a native Windows SSH target instead of Parallels
|
|
60
|
+
PLATFORM_SMOKE_WINDOWS_PORT Windows SSH port (default: 22)
|
|
56
61
|
PLATFORM_SMOKE_WINDOWS_USER Windows SSH user (default: $USER)
|
|
57
62
|
PLATFORM_SMOKE_WINDOWS_WORK_ROOT Windows work root
|
|
58
63
|
PI_ORACLE_SMOKE_* Project-specific aliases for the PLATFORM_SMOKE_* knobs above
|
|
64
|
+
PI_ORACLE_RELEASE_TARBALL Use this retained tarball for the packed platform suites
|
|
65
|
+
PI_ORACLE_RELEASE_SHA256 Expected SHA-256 of the retained tarball, when supplied
|
|
59
66
|
PI_ORACLE_REAL_TEST_PROVIDER Real smoke provider (default: zai)
|
|
60
67
|
PI_ORACLE_REAL_TEST_MODEL Real smoke model (default: glm-5.2)
|
|
61
68
|
ZAI_API_KEY Default real-smoke provider API key for optional model-agent debugging
|
|
@@ -104,9 +111,9 @@ function validateSelection(targets, suites) {
|
|
|
104
111
|
}
|
|
105
112
|
}
|
|
106
113
|
|
|
107
|
-
async function runDoctor() {
|
|
114
|
+
async function runDoctor(targets) {
|
|
108
115
|
const { runDoctor } = await import("./platform-smoke/doctor.mjs");
|
|
109
|
-
await runDoctor(config);
|
|
116
|
+
await runDoctor({ ...config, requiredTargets: targets });
|
|
110
117
|
}
|
|
111
118
|
|
|
112
119
|
async function runTarget(targetName, suites, singleSuite) {
|
|
@@ -124,7 +131,10 @@ async function main() {
|
|
|
124
131
|
if (!config) throw new Error("platform-smoke.config.mjs not found or failed to load");
|
|
125
132
|
|
|
126
133
|
if (args.command === "doctor") {
|
|
127
|
-
|
|
134
|
+
const targets = args.target ? splitCsv(args.target) : config.requiredTargets;
|
|
135
|
+
if (targets.length === 0) throw new Error("--target did not resolve to any targets");
|
|
136
|
+
validateSelection(targets, config.requiredSuites);
|
|
137
|
+
await runDoctor(targets);
|
|
128
138
|
return;
|
|
129
139
|
}
|
|
130
140
|
|
|
@@ -134,13 +144,36 @@ async function main() {
|
|
|
134
144
|
if (targets.length === 0) throw new Error("--target did not resolve to any targets");
|
|
135
145
|
if (suites.length === 0) throw new Error("--suite did not resolve to any suites");
|
|
136
146
|
validateSelection(targets, suites);
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
147
|
+
let inputDir;
|
|
148
|
+
try {
|
|
149
|
+
if (process.env.PI_ORACLE_RELEASE_TARBALL) {
|
|
150
|
+
// Crabbox syncs nonignored files, but this repository ignores *.tgz.
|
|
151
|
+
inputDir = mkdtempSync(resolve(repoRoot, ".oracle-release-input-"));
|
|
152
|
+
const candidate = resolve(inputDir, "candidate");
|
|
153
|
+
copyFileSync(resolve(process.env.PI_ORACLE_RELEASE_TARBALL), candidate);
|
|
154
|
+
const sha256 = createHash("sha256").update(readFileSync(candidate)).digest("hex");
|
|
155
|
+
if (process.env.PI_ORACLE_RELEASE_SHA256 && sha256 !== process.env.PI_ORACLE_RELEASE_SHA256) {
|
|
156
|
+
throw new Error("Retained release candidate checksum changed");
|
|
157
|
+
}
|
|
158
|
+
config.releaseCandidate = {
|
|
159
|
+
path: relative(repoRoot, candidate).split(sep).join("/"),
|
|
160
|
+
sha256,
|
|
161
|
+
};
|
|
162
|
+
console.log(`Retained release candidate SHA-256: ${config.releaseCandidate.sha256}`);
|
|
163
|
+
}
|
|
164
|
+
const outcomes = await Promise.allSettled(targets.map(async (target) => {
|
|
165
|
+
console.log(`\n=== Target: ${target} ===`);
|
|
166
|
+
return { target, result: await runTarget(target, suites, Boolean(args.suite)) };
|
|
167
|
+
}));
|
|
168
|
+
const failure = outcomes.find((outcome) => outcome.status === "rejected");
|
|
169
|
+
if (failure) throw failure.reason;
|
|
170
|
+
const results = outcomes.map((outcome) => outcome.value);
|
|
171
|
+
if (results.some(({ result }) => !result.ok)) {
|
|
172
|
+
console.log("\nOne or more platform smoke suites failed. Check .artifacts/platform-smoke/ for details.");
|
|
173
|
+
process.exitCode = 1;
|
|
174
|
+
}
|
|
175
|
+
} finally {
|
|
176
|
+
if (inputDir) rmSync(inputDir, { recursive: true, force: true });
|
|
144
177
|
}
|
|
145
178
|
return;
|
|
146
179
|
}
|