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
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { execFileSync } from "node:child_process";
|
|
3
|
+
import { createCipheriv, createHash, pbkdf2Sync } from "node:crypto";
|
|
4
|
+
import { mkdtemp, mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
5
|
+
import { tmpdir } from "node:os";
|
|
6
|
+
import { join, relative } from "node:path";
|
|
7
|
+
import test from "node:test";
|
|
8
|
+
|
|
9
|
+
// Independent standard Chromium v24 SQLite/v11 fixture; the real auth worker
|
|
10
|
+
// reads/decrypts/filters it and replays into a fake browser, never a real one.
|
|
11
|
+
test("Linux auth imports native Chromium with its own key and preserves Chrome/Brave", {
|
|
12
|
+
skip: process.platform !== "linux", timeout: 90_000,
|
|
13
|
+
}, async (t) => {
|
|
14
|
+
const { DatabaseSync } = await import("node:sqlite");
|
|
15
|
+
const { detectDefaultLinuxCookieProfileSource } = await import("../extensions/oracle/shared/browser-profile-helpers.mjs");
|
|
16
|
+
const root = await mkdtemp(join(tmpdir(), "oracle-cookie-import-"));
|
|
17
|
+
t.after(() => rm(root, { recursive: true, force: true }));
|
|
18
|
+
const home = join(root, "home");
|
|
19
|
+
const configHome = join(home, ".config");
|
|
20
|
+
const bin = join(root, "bin");
|
|
21
|
+
const helperLog = join(root, "helpers.jsonl");
|
|
22
|
+
const browserLog = join(root, "browser.jsonl");
|
|
23
|
+
await mkdir(bin, { recursive: true });
|
|
24
|
+
const guard = `
|
|
25
|
+
import { appendFileSync, mkdirSync } from 'node:fs';
|
|
26
|
+
import { basename } from 'node:path';
|
|
27
|
+
for (const [name, value] of Object.entries(process.env)) {
|
|
28
|
+
if (name.startsWith('SWEET_COOKIE_') && name.endsWith('_PASSWORD') && value) {
|
|
29
|
+
throw Error('safe-storage password leaked to subprocess');
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
const args = process.argv.slice(2);
|
|
33
|
+
`;
|
|
34
|
+
const helper = `#!${process.execPath}
|
|
35
|
+
${guard}
|
|
36
|
+
const command = basename(process.argv[1]);
|
|
37
|
+
appendFileSync(process.env.HELPER_LOG, JSON.stringify({ command, args }) + '\\n');
|
|
38
|
+
const mode = process.env.FIXTURE_MODE;
|
|
39
|
+
if (mode === 'timeout') { process.on('SIGTERM', () => {}); setInterval(() => {}, 1000); }
|
|
40
|
+
else if (mode === 'failure') { console.error('fixture-key-do-not-log'); process.exit(1); }
|
|
41
|
+
else if (command === 'dbus-send') {
|
|
42
|
+
if (mode === 'wallet-fallback') process.exit(1);
|
|
43
|
+
console.log('"fixture-wallet"');
|
|
44
|
+
} else if (command === 'kwallet-query') {
|
|
45
|
+
if (mode === 'wallet-empty') console.log('failed to read password');
|
|
46
|
+
else if (args.join('|') === '--read-password|Chromium Safe Storage|--folder|Chromium Keys|' + (mode === 'wallet-fallback' ? 'kdewallet' : 'fixture-wallet')) console.log('fixture-chromium-key');
|
|
47
|
+
else process.exit(1);
|
|
48
|
+
} else {
|
|
49
|
+
if (mode === 'application' && args.includes('service')) process.exit(1);
|
|
50
|
+
const browser = args.includes('Chromium') || args.includes('chromium') ? 'chromium'
|
|
51
|
+
: args.includes('Brave') || args.includes('brave') ? 'brave' : 'chrome';
|
|
52
|
+
console.log('fixture-' + browser + '-key');
|
|
53
|
+
}
|
|
54
|
+
`;
|
|
55
|
+
for (const command of ["secret-tool", "kwallet-query", "dbus-send"]) {
|
|
56
|
+
await writeFile(join(bin, command), helper, { mode: 0o700 });
|
|
57
|
+
}
|
|
58
|
+
await writeFile(join(bin, "agent-browser"), `#!${process.execPath}
|
|
59
|
+
${guard}
|
|
60
|
+
appendFileSync(process.env.BROWSER_LOG, JSON.stringify(args) + '\\n');
|
|
61
|
+
if (args.includes('--profile')) mkdirSync(args[args.indexOf('--profile') + 1], { recursive: true });
|
|
62
|
+
if (args.includes('status')) console.log(JSON.stringify({ data: { connected: true } }));
|
|
63
|
+
else if (args.includes('snapshot')) console.log('- textbox "Chat with ChatGPT" [ref=e1]\\n- button "Add files and more" [ref=e2]');
|
|
64
|
+
else if (args.includes('eval')) { process.stdin.resume(); process.stdin.on('end', () => console.log(JSON.stringify({ ok: true, status: 200, bodyHasId: true, bodyHasEmail: true }))); }
|
|
65
|
+
else if (args.includes('url')) console.log('https://chatgpt.com/');
|
|
66
|
+
`, { mode: 0o700 });
|
|
67
|
+
|
|
68
|
+
const cases = [
|
|
69
|
+
{ browser: "google-chrome", password: "fixture-chrome-key" },
|
|
70
|
+
{ browser: "BraveSoftware/Brave-Browser", password: "fixture-brave-key" },
|
|
71
|
+
{ browser: "chromium", password: "fixture-chromium-key" },
|
|
72
|
+
{ browser: "chromium", password: "fixture-chromium-key", auto: true },
|
|
73
|
+
{ browser: "chromium", password: "fixture-chromium-key", relative: true },
|
|
74
|
+
{ browser: "chromium", password: "fixture-chromium-key", braveAncestor: true },
|
|
75
|
+
{ browser: "chromium", password: "fixture-override-key", braveAncestor: true, override: true },
|
|
76
|
+
{ browser: "chromium", password: "fixture-chromium-key", db: true, network: true },
|
|
77
|
+
{ browser: "chromium-browser", password: "fixture-chromium-key", mode: "application" },
|
|
78
|
+
{ browser: "chromium-browser", password: "fixture-chromium-key", db: true },
|
|
79
|
+
...["6", "5", ""].map((kde) => ({ browser: "chromium", password: "fixture-chromium-key", backend: "kwallet", kde })),
|
|
80
|
+
{ browser: "chromium", password: "fixture-chromium-key", backend: "kwallet", mode: "wallet-fallback" },
|
|
81
|
+
{ browser: "chromium", password: "", backend: "kwallet", mode: "failure" },
|
|
82
|
+
{ browser: "chromium", password: "", backend: "kwallet", mode: "wallet-empty" },
|
|
83
|
+
{ browser: "chromium", password: "peanuts", backend: "basic", prefix: "v10" },
|
|
84
|
+
{ browser: "chromium", password: "", backend: "basic" },
|
|
85
|
+
{ browser: "chromium", password: "fixture-override-key", override: true },
|
|
86
|
+
{ browser: "chromium", password: "fixture-chromium-key", backend: "invalid", desktop: "GNOME:KDE", kde: "6" },
|
|
87
|
+
{ browser: "chromium", password: "", mode: "failure" },
|
|
88
|
+
{ browser: "chromium", password: "", mode: "timeout" },
|
|
89
|
+
{ browser: "google-chrome", password: "fixture-chrome-key", profileName: true },
|
|
90
|
+
{ browser: "chromium", password: "fixture-override-key", braveAncestor: true, override: true, relative: true },
|
|
91
|
+
{ browser: "chromium", password: "fixture-override-key", braveAncestor: true, override: true, tilde: true },
|
|
92
|
+
{ browser: "chromium", password: "fixture-override-key", override: true, db: true, dotSegment: true },
|
|
93
|
+
];
|
|
94
|
+
for (const [index, c] of cases.entries()) {
|
|
95
|
+
// Keep the release artifact scanner strict: fixture password values stay in
|
|
96
|
+
// this file, out of captured platform-smoke stdout (case flags stay unique).
|
|
97
|
+
const { password: _fixturePassword, ...label } = c;
|
|
98
|
+
await t.test(JSON.stringify(label), async () => {
|
|
99
|
+
const sourceHome = c.tilde ? join(root, "BraveSoftware", "home") : home;
|
|
100
|
+
const sourceConfigHome = c.tilde ? join(sourceHome, ".config")
|
|
101
|
+
: c.braveAncestor ? join(root, "BraveSoftware", "config")
|
|
102
|
+
: c.auto ? join(root, "xdg-native") : configHome;
|
|
103
|
+
const profile = join(sourceConfigHome, c.browser, "Default");
|
|
104
|
+
const dbPath = join(profile, ...(c.network ? ["Network"] : []), "Cookies");
|
|
105
|
+
await mkdir(join(profile, ...(c.network ? ["Network"] : [])), { recursive: true });
|
|
106
|
+
if (c.dotSegment) await mkdir(join(sourceConfigHome, "BraveSoftware"), { recursive: true });
|
|
107
|
+
const cookiePath = c.dotSegment ? `${sourceConfigHome}/BraveSoftware/../${c.browser}/Default/Cookies` : dbPath;
|
|
108
|
+
await rm(join(profile, "Cookies"), { force: true });
|
|
109
|
+
const db = new DatabaseSync(dbPath);
|
|
110
|
+
try {
|
|
111
|
+
db.exec(`CREATE TABLE meta (key TEXT PRIMARY KEY, value TEXT);
|
|
112
|
+
INSERT INTO meta VALUES ('version', '24');
|
|
113
|
+
CREATE TABLE cookies (name TEXT, value TEXT, host_key TEXT, path TEXT,
|
|
114
|
+
expires_utc INTEGER, samesite INTEGER, encrypted_value BLOB,
|
|
115
|
+
is_secure INTEGER, is_httponly INTEGER);`);
|
|
116
|
+
const cipher = createCipheriv("aes-128-cbc", pbkdf2Sync(c.password, "saltysalt", 1, 16, "sha1"), Buffer.alloc(16, 32));
|
|
117
|
+
const plaintext = Buffer.concat([createHash("sha256").update(".chatgpt.com").digest(), Buffer.from("fixture-session-token")]);
|
|
118
|
+
const encrypted = Buffer.concat([Buffer.from(c.prefix ?? "v11"), cipher.update(plaintext), cipher.final()]);
|
|
119
|
+
db.prepare("INSERT INTO cookies VALUES (?, '', '.chatgpt.com', '/', 0, 1, ?, 1, 1)")
|
|
120
|
+
.run("__Secure-next-auth.session-token", encrypted);
|
|
121
|
+
} finally { db.close(); }
|
|
122
|
+
await writeFile(helperLog, "");
|
|
123
|
+
await writeFile(browserLog, "");
|
|
124
|
+
const config = {
|
|
125
|
+
defaults: { provider: "chatgpt", preset: "instant" },
|
|
126
|
+
browser: { authSeedProfileDir: join(root, `seed-${index}`), sessionPrefix: "fixture", chatUrl: "https://chatgpt.com/" },
|
|
127
|
+
auth: { chromeProfile: c.auto ? detectDefaultLinuxCookieProfileSource({ homeDir: sourceHome, env: { XDG_CONFIG_HOME: sourceConfigHome } })
|
|
128
|
+
: c.profileName ? "Default" : c.relative ? "./Default" : c.tilde ? `~/${relative(sourceHome, profile)}` : profile,
|
|
129
|
+
...(c.db ? { chromeCookiePath: cookiePath } : {}), bootstrapTimeoutMs: 1000, pollMs: 1 },
|
|
130
|
+
};
|
|
131
|
+
const env = { HOME: sourceHome, XDG_CONFIG_HOME: sourceConfigHome, PATH: bin, TMPDIR: root,
|
|
132
|
+
AGENT_BROWSER_PATH: join(bin, "agent-browser"), PI_ORACLE_STATE_DIR: join(root, "state"),
|
|
133
|
+
SWEET_COOKIE_LINUX_KEYRING: c.backend ?? "gnome", KDE_SESSION_VERSION: c.kde ?? "",
|
|
134
|
+
XDG_CURRENT_DESKTOP: c.desktop ?? "",
|
|
135
|
+
...(c.override ? { SWEET_COOKIE_CHROME_SAFE_STORAGE_PASSWORD: c.password } : {}),
|
|
136
|
+
FIXTURE_MODE: c.mode ?? "", HELPER_LOG: helperLog, BROWSER_LOG: browserLog };
|
|
137
|
+
const output = execFileSync(process.execPath, [new URL("../extensions/oracle/worker/auth-bootstrap.mjs", import.meta.url).pathname, JSON.stringify(config)], {
|
|
138
|
+
encoding: "utf8", timeout: 15_000, stdio: ["ignore", "pipe", "pipe"], env,
|
|
139
|
+
...(c.relative || c.profileName ? { cwd: join(sourceConfigHome, "chromium") } : {}),
|
|
140
|
+
});
|
|
141
|
+
assert.match(output, /Oracle auth synced/);
|
|
142
|
+
const replay = (await readFile(browserLog, "utf8")).trim().split("\n").map((line) => JSON.parse(line));
|
|
143
|
+
assert(replay.some((args) => args.includes("__Secure-next-auth.session-token") && args.includes("fixture-session-token")), "production import must decrypt and replay the known cookie");
|
|
144
|
+
const calls = (await readFile(helperLog, "utf8")).trim();
|
|
145
|
+
if (c.override || c.backend === "basic") assert.equal(calls, "");
|
|
146
|
+
else if (c.browser.startsWith("chromium")) {
|
|
147
|
+
assert.doesNotMatch(calls, /Chrome Safe Storage|application","chrome"/);
|
|
148
|
+
if (c.mode === "application") assert.match(calls, /application","chromium/);
|
|
149
|
+
if (c.backend === "kwallet") {
|
|
150
|
+
assert.match(calls, new RegExp(`org.kde.kwalletd${c.kde ?? ""}`));
|
|
151
|
+
assert.match(calls, /Chromium Keys/);
|
|
152
|
+
assert.match(calls, ["wallet-fallback", "failure"].includes(c.mode) ? /kdewallet/ : /fixture-wallet/);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
const diagnostics = output.match(/Diagnostics:\n([^\n]+)/)[1];
|
|
156
|
+
const log = await readFile(join(diagnostics, "oracle-auth.log"), "utf8");
|
|
157
|
+
assert.doesNotMatch(log + output, /fixture-(?:chromium|override)-key|fixture-key-do-not-log/);
|
|
158
|
+
if (["failure", "timeout"].includes(c.mode)) assert.match(log, /Failed to read Linux keyring via (secret-tool|kwallet-query)/);
|
|
159
|
+
if (index === 2 || (c.braveAncestor && !c.override)) {
|
|
160
|
+
// A prior successful Chrome read must not cache the wrong key. The
|
|
161
|
+
// adapter's legitimate API must restore env on success AND rejection.
|
|
162
|
+
execFileSync(process.execPath, ["--input-type=module", "-e", `
|
|
163
|
+
import assert from 'node:assert/strict';
|
|
164
|
+
import { getCookies } from '@steipete/sweet-cookie';
|
|
165
|
+
import { getCookiesFromNativeLinuxChromium } from './extensions/oracle/worker/linux-chromium-cookie-source.mjs';
|
|
166
|
+
const options = {url:'https://chatgpt.com/',browsers:['chrome'],mode:'merge',chromeProfile:process.argv[1]};
|
|
167
|
+
assert.equal((await getCookies({...options,chromeProfile:process.argv[2]})).cookies[0].value,'fixture-session-token');
|
|
168
|
+
process.env.SWEET_COOKIE_CHROME_SAFE_STORAGE_PASSWORD = ' ';
|
|
169
|
+
process.env.SWEET_COOKIE_BRAVE_SAFE_STORAGE_PASSWORD = 'fixture-unused-brave-password';
|
|
170
|
+
const previous = {...process.env};
|
|
171
|
+
assert.equal((await getCookiesFromNativeLinuxChromium(options, getCookies)).cookies[0].value,'fixture-session-token');
|
|
172
|
+
assert.deepEqual({...process.env},previous);
|
|
173
|
+
await assert.rejects(getCookiesFromNativeLinuxChromium({...options,origins:[null]}, getCookies),TypeError);
|
|
174
|
+
assert.deepEqual({...process.env},previous);
|
|
175
|
+
process.env.FIXTURE_MODE = 'failure';
|
|
176
|
+
const failureEnv = {...process.env};
|
|
177
|
+
const failedKeyring = await getCookiesFromNativeLinuxChromium(options, getCookies);
|
|
178
|
+
assert.deepEqual(failedKeyring.cookies,[]);
|
|
179
|
+
assert.match(failedKeyring.warnings.join('|'),/Failed to read Linux keyring via secret-tool/);
|
|
180
|
+
assert.deepEqual({...process.env},failureEnv);
|
|
181
|
+
await assert.rejects(getCookiesFromNativeLinuxChromium({...options,origins:[null]}, getCookies),TypeError);
|
|
182
|
+
assert.deepEqual({...process.env},failureEnv);
|
|
183
|
+
process.env.SWEET_COOKIE_CHROME_SAFE_STORAGE_PASSWORD = ' fixture-chromium-key ';
|
|
184
|
+
const calibratedEnv = {...process.env};
|
|
185
|
+
assert.equal((await getCookiesFromNativeLinuxChromium(options, getCookies)).cookies[0].value,'fixture-session-token');
|
|
186
|
+
assert.deepEqual({...process.env},calibratedEnv);
|
|
187
|
+
await assert.rejects(getCookiesFromNativeLinuxChromium({...options,origins:[null]}, getCookies),TypeError);
|
|
188
|
+
assert.deepEqual({...process.env},calibratedEnv);
|
|
189
|
+
delete process.env.SWEET_COOKIE_CHROME_SAFE_STORAGE_PASSWORD;
|
|
190
|
+
process.env.SWEET_COOKIE_BRAVE_SAFE_STORAGE_PASSWORD = 'fixture-chromium-key';
|
|
191
|
+
process.env.SWEET_COOKIE_LINUX_KEYRING = 'basic';
|
|
192
|
+
const basicEnv = {...process.env};
|
|
193
|
+
assert.deepEqual((await getCookiesFromNativeLinuxChromium(options, getCookies)).cookies,[]);
|
|
194
|
+
assert.deepEqual({...process.env},basicEnv);
|
|
195
|
+
await assert.rejects(getCookiesFromNativeLinuxChromium({...options,origins:[null]}, getCookies),TypeError);
|
|
196
|
+
assert.deepEqual({...process.env},basicEnv);
|
|
197
|
+
`, profile, join(configHome, "google-chrome", "Default")], {
|
|
198
|
+
cwd: new URL("../", import.meta.url), encoding: "utf8", timeout: 10_000, env,
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
await rm(join(profile, "Network"), { recursive: true, force: true });
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
});
|
|
@@ -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,
|
|
@@ -38,7 +39,11 @@ function harness({ initial = 3, max = "4", wrongLabel = false, stuck = false, mo
|
|
|
38
39
|
composerControlsVisible: ui.snapshotHasUsableComposerControls,
|
|
39
40
|
log: async (line) => { if (line.startsWith("Verified ChatGPT Power tier")) verified = true; },
|
|
40
41
|
agentBrowser: async (_job, command, arg) => {
|
|
41
|
-
if (command === "focus") {
|
|
42
|
+
if (command === "focus") {
|
|
43
|
+
assert(open);
|
|
44
|
+
assert(mountingReads < 0, "wait for the Power slider to mount before focusing it");
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
42
47
|
if (command !== "press") return;
|
|
43
48
|
if (arg === "Escape") open = false;
|
|
44
49
|
else if (!stuck) value = Math.max(0, Math.min(4, value + (arg === "ArrowRight" ? 1 : -1)));
|
|
@@ -54,19 +59,21 @@ function harness({ initial = 3, max = "4", wrongLabel = false, stuck = false, mo
|
|
|
54
59
|
}
|
|
55
60
|
|
|
56
61
|
test("worker configures every preset through the current Power picker and closes it", async () => {
|
|
57
|
-
for (const [
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
+
}
|
|
70
77
|
}
|
|
71
78
|
});
|
|
72
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 });
|
|
@@ -287,6 +298,11 @@ async function preparePackedProject({ runDir, provider, model, timeoutMs }) {
|
|
|
287
298
|
writeFileSync(join(piProject, ".artifacts", "ignored", "artifact.txt"), "ignore me\n");
|
|
288
299
|
mkdirSync(join(piProject, ".crabbox", "ignored"), { recursive: true });
|
|
289
300
|
writeFileSync(join(piProject, ".crabbox", "ignored", "capture.txt"), "ignore me\n");
|
|
301
|
+
mkdirSync(join(piProject, ".pi"), { recursive: true });
|
|
302
|
+
writeFileSync(join(piProject, ".pi", "workspace-notes.txt"), "ignore me\n");
|
|
303
|
+
mkdirSync(join(piProject, ".oracle-context"), { recursive: true });
|
|
304
|
+
writeFileSync(join(piProject, ".oracle-context", "context.md"), "ignore me\n");
|
|
305
|
+
writeFileSync(join(piProject, ".scratchpad.md"), "ignore me\n");
|
|
290
306
|
|
|
291
307
|
return { mode: "packed", cwd: piProject, installDir, provider, model, extensionPath: `./node_modules/${PACKAGE_NAME}` };
|
|
292
308
|
}
|
|
@@ -369,11 +385,6 @@ async function run(mode = "packed") {
|
|
|
369
385
|
const piVersion = await mustRun(runDir, "pi-version", piCommand(), ["--version"], { cwd: process.cwd(), env: process.env, timeoutMs: 30_000 });
|
|
370
386
|
if (piVersion.stdout.trim() !== EXPECTED_PI_VERSION) throw new Error(`real smoke requires Pi ${EXPECTED_PI_VERSION}; found ${piVersion.stdout.trim() || "unknown"}`);
|
|
371
387
|
const tmpRoot = mkdtempSync(join(tmpdir(), "pi-oracle-real-smoke-"));
|
|
372
|
-
const prepared = mode === "packed"
|
|
373
|
-
? await preparePackedProject({ runDir, provider, model, timeoutMs })
|
|
374
|
-
: prepareSourceProject({ provider, model });
|
|
375
|
-
console.log(`Oracle real smoke mode=${prepared.mode} extension=${prepared.extensionPath}`);
|
|
376
|
-
writeFileSync(join(runDir, "run.json"), `${JSON.stringify({ runId, mode: prepared.mode, provider, model, extensionPath: prepared.extensionPath, timeoutMs, startedAt: new Date().toISOString() }, null, 2)}\n`);
|
|
377
388
|
|
|
378
389
|
function assert(id, condition, message) {
|
|
379
390
|
assertions.push({ id, ok: Boolean(condition), ...(condition ? {} : { error: message }) });
|
|
@@ -381,6 +392,12 @@ async function run(mode = "packed") {
|
|
|
381
392
|
}
|
|
382
393
|
|
|
383
394
|
try {
|
|
395
|
+
const prepared = mode === "packed"
|
|
396
|
+
? await preparePackedProject({ runDir, provider, model, timeoutMs })
|
|
397
|
+
: prepareSourceProject({ provider, model });
|
|
398
|
+
console.log(`Oracle real smoke mode=${prepared.mode} extension=${prepared.extensionPath}`);
|
|
399
|
+
writeFileSync(join(runDir, "run.json"), `${JSON.stringify({ runId, mode: prepared.mode, provider, model, extensionPath: prepared.extensionPath, timeoutMs, startedAt: new Date().toISOString() }, null, 2)}\n`);
|
|
400
|
+
|
|
384
401
|
const test1 = join(runDir, "whole-repo-submit");
|
|
385
402
|
const agent1 = join(runDir, "agent1");
|
|
386
403
|
const sessions1 = join(runDir, "sessions1");
|
|
@@ -423,7 +440,7 @@ async function run(mode = "packed") {
|
|
|
423
440
|
const entries = await tarList(archivePath);
|
|
424
441
|
writeFileSync(join(test1, "archive-list.txt"), `${entries.join("\n")}\n`);
|
|
425
442
|
assert("archive-includes-readme", entryExists(entries, "README.md"), "archive should include README.md");
|
|
426
|
-
for (const excluded of [".pi", ".oracle-context", ".scratchpad.md", ".artifacts", ".crabbox"
|
|
443
|
+
for (const excluded of [".pi", ".oracle-context", ".scratchpad.md", ".artifacts", ".crabbox"]) {
|
|
427
444
|
assert(`archive-excludes-${excluded.replace(/[^a-z0-9]+/gi, "-")}`, !entryExists(entries, excluded), `archive should exclude ${excluded}`);
|
|
428
445
|
}
|
|
429
446
|
} else {
|
|
@@ -466,7 +483,11 @@ async function run(mode = "packed") {
|
|
|
466
483
|
const jobDir2 = latestJobDir(jobs2);
|
|
467
484
|
writeFileSync(join(test2, "job-dir.txt"), `${jobDir2 ?? ""}\n`);
|
|
468
485
|
const symlinkOutput = `${symlinkResult.stdout}\n${symlinkResult.stderr}`;
|
|
469
|
-
assert(
|
|
486
|
+
assert(
|
|
487
|
+
"symlink-rejected",
|
|
488
|
+
symlinkOutput.includes("archive_input_symlink_escape") || symlinkOutput.includes("Archive input must resolve inside the project cwd without symlink escapes"),
|
|
489
|
+
"symlink escape test output did not show the actual oracle_submit rejection evidence",
|
|
490
|
+
);
|
|
470
491
|
assert("symlink-no-job-created", !jobDir2, "symlink escape rejection should not create an oracle job");
|
|
471
492
|
}
|
|
472
493
|
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
FROM cimg/node:24.
|
|
1
|
+
FROM cimg/node:24.21
|
|
2
2
|
|
|
3
3
|
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
|
|
7
|
+
RUN npm install -g npm \
|
|
8
|
+
&& npm install -g --allow-scripts=agent-browser agent-browser
|
|
8
9
|
USER circleci
|
|
@@ -14,8 +14,7 @@ export function execCrabbox(args, opts = {}) {
|
|
|
14
14
|
return new Promise((resolve) => {
|
|
15
15
|
const child = spawn(CRABBOX_BIN, args, {
|
|
16
16
|
stdio: ["ignore", "pipe", "pipe"],
|
|
17
|
-
env: { ...process.env, CRABBOX_SYNC_GIT_SEED: "false"
|
|
18
|
-
...opts.spawnOpts,
|
|
17
|
+
env: { ...process.env, CRABBOX_SYNC_GIT_SEED: "false" },
|
|
19
18
|
});
|
|
20
19
|
const stdoutChunks = [];
|
|
21
20
|
const stderrChunks = [];
|
|
@@ -59,6 +58,7 @@ export function buildTargetBaseArgs(targetName, config = {}) {
|
|
|
59
58
|
switch (targetName) {
|
|
60
59
|
case "macos": {
|
|
61
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";
|
|
62
62
|
const user = env("PI_ORACLE_SMOKE_MAC_USER") || env("PLATFORM_SMOKE_MAC_USER") || env("USER");
|
|
63
63
|
const workRoot = env("PI_ORACLE_SMOKE_MAC_WORK_ROOT") || env("PLATFORM_SMOKE_MAC_WORK_ROOT") || `/Users/${env("USER")}/crabbox/${config.packageName ?? "pi-oracle"}`;
|
|
64
64
|
return [
|
|
@@ -66,7 +66,7 @@ export function buildTargetBaseArgs(targetName, config = {}) {
|
|
|
66
66
|
"--target", "macos",
|
|
67
67
|
"--static-host", host,
|
|
68
68
|
"--static-user", user,
|
|
69
|
-
"--static-port",
|
|
69
|
+
"--static-port", port,
|
|
70
70
|
"--static-work-root", workRoot,
|
|
71
71
|
];
|
|
72
72
|
}
|
|
@@ -83,6 +83,19 @@ export function buildTargetBaseArgs(targetName, config = {}) {
|
|
|
83
83
|
const snapshot = env("PI_ORACLE_SMOKE_WINDOWS_SNAPSHOT") || env("PLATFORM_SMOKE_WINDOWS_SNAPSHOT") || config.windowsParallels?.snapshot || "crabbox-ready";
|
|
84
84
|
const user = env("PI_ORACLE_SMOKE_WINDOWS_USER") || env("PLATFORM_SMOKE_WINDOWS_USER") || env("USER");
|
|
85
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
|
+
}
|
|
86
99
|
return [
|
|
87
100
|
"--provider", "parallels",
|
|
88
101
|
"--target", "windows",
|
|
@@ -122,10 +135,9 @@ export async function runOnLease(config, targetName, leaseId, command, opts = {}
|
|
|
122
135
|
for (const name of opts.allowEnvNames ?? []) args.push("--allow-env", name);
|
|
123
136
|
if (opts.sync === false) args.push("--no-sync");
|
|
124
137
|
else args.push("--fresh-sync");
|
|
125
|
-
|
|
126
|
-
else args.push("--", ...(Array.isArray(command) ? command : command.split(" ")));
|
|
138
|
+
args.push("--shell", command);
|
|
127
139
|
console.log(` [crabbox] run ${targetName} ${opts.sync === false ? "--no-sync" : "--fresh-sync"} ...`);
|
|
128
|
-
return execCrabbox(args, { timeout: opts.timeout ?? 900_000
|
|
140
|
+
return execCrabbox(args, { timeout: opts.timeout ?? 900_000 });
|
|
129
141
|
}
|
|
130
142
|
|
|
131
143
|
export async function stopLease(config, targetName, leaseId) {
|
|
@@ -1,8 +1,9 @@
|
|
|
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
|
+
import { buildTargetBaseArgs } from "./crabbox-runner.mjs";
|
|
6
7
|
|
|
7
8
|
let failures = 0;
|
|
8
9
|
|
|
@@ -63,24 +64,6 @@ function parseJson(text) {
|
|
|
63
64
|
try { return JSON.parse(text); } catch { return undefined; }
|
|
64
65
|
}
|
|
65
66
|
|
|
66
|
-
function targetBaseArgs(targetName, config) {
|
|
67
|
-
if (targetName === "macos") {
|
|
68
|
-
const host = env("PI_ORACLE_SMOKE_MAC_HOST") || env("PLATFORM_SMOKE_MAC_HOST") || "localhost";
|
|
69
|
-
const user = env("PI_ORACLE_SMOKE_MAC_USER") || env("PLATFORM_SMOKE_MAC_USER") || env("USER");
|
|
70
|
-
const workRoot = env("PI_ORACLE_SMOKE_MAC_WORK_ROOT") || env("PLATFORM_SMOKE_MAC_WORK_ROOT") || `/Users/${env("USER")}/crabbox/${config.packageName}`;
|
|
71
|
-
return ["--provider", "ssh", "--target", "macos", "--static-host", host, "--static-user", user, "--static-port", "22", "--static-work-root", workRoot];
|
|
72
|
-
}
|
|
73
|
-
if (targetName === "ubuntu") {
|
|
74
|
-
const image = env("PI_ORACLE_SMOKE_UBUNTU_IMAGE") || env("PLATFORM_SMOKE_UBUNTU_IMAGE") || config.ubuntuContainerImage || "pi-oracle-platform-smoke:node24";
|
|
75
|
-
return ["--provider", "local-container", "--target", "linux", "--local-container-image", image];
|
|
76
|
-
}
|
|
77
|
-
const vm = windowsVmName(config);
|
|
78
|
-
const snapshot = windowsSnapshotName(config);
|
|
79
|
-
const user = env("PI_ORACLE_SMOKE_WINDOWS_USER") || env("PLATFORM_SMOKE_WINDOWS_USER") || env("USER");
|
|
80
|
-
const workRoot = env("PI_ORACLE_SMOKE_WINDOWS_NATIVE_WORK_ROOT") || env("PLATFORM_SMOKE_WINDOWS_WORK_ROOT") || `C:\\crabbox\\${config.packageName}`;
|
|
81
|
-
return ["--provider", "parallels", "--target", "windows", "--windows-mode", "normal", "--parallels-source", vm, "--parallels-source-snapshot", snapshot, "--parallels-user", user, "--parallels-work-root", workRoot];
|
|
82
|
-
}
|
|
83
|
-
|
|
84
67
|
function runCrabboxDoctor(cbox, label, args, timeout = 120_000) {
|
|
85
68
|
const output = silent(cbox, ["doctor", ...args, "--json"], { env: { ...process.env, CRABBOX_SYNC_GIT_SEED: "false" }, timeout });
|
|
86
69
|
if (!output) {
|
|
@@ -100,7 +83,7 @@ function runTargetToolProbe(cbox, targetName, config) {
|
|
|
100
83
|
const command = targetName === "windows-native"
|
|
101
84
|
? `cmd.exe /c "where node && where npm && where git && where tar && where zstd && where agent-browser && zstd --version && agent-browser --version && echo tools-ok"`
|
|
102
85
|
: `missing=""; for tool in ${required.join(" ")}; do command -v "$tool" >/dev/null 2>&1 || missing="$missing $tool"; done; if [ -n "$missing" ]; then echo "missing=$missing"; exit 1; fi; zstd --version >/dev/null && agent-browser --version >/dev/null && echo tools-ok`;
|
|
103
|
-
const baseArgs =
|
|
86
|
+
const baseArgs = buildTargetBaseArgs(targetName, config);
|
|
104
87
|
if (targetName === "macos") baseArgs.push("--reclaim");
|
|
105
88
|
const result = silent(cbox, ["run", ...baseArgs, "--no-sync", "--shell", command], {
|
|
106
89
|
env: { ...process.env, CRABBOX_SYNC_GIT_SEED: "false" },
|
|
@@ -137,37 +120,64 @@ function verifyPackageExclusions() {
|
|
|
137
120
|
const forbiddenPatterns = [/^\.env(?:\.|$)/, /^\.artifacts(?:\/|$)/, /^\.crabbox(?:\/|$)/, /^\.debug(?:\/|$)/, /^\.platform-smoke-runs(?:\/|$)/, /\.tgz$/];
|
|
138
121
|
const output = shell("npm pack --dry-run --json", { timeout: 120_000 });
|
|
139
122
|
const parsed = parseJson(output);
|
|
140
|
-
|
|
123
|
+
// npm 11 returns an array; npm 12 keys the same records by package name.
|
|
124
|
+
const [pack] = parsed && typeof parsed === "object" ? Object.values(parsed) : [];
|
|
125
|
+
if (!pack?.files) {
|
|
141
126
|
warn("could not inspect npm pack file list");
|
|
142
127
|
return;
|
|
143
128
|
}
|
|
144
|
-
const files =
|
|
129
|
+
const files = pack.files.map((file) => file.path);
|
|
145
130
|
const forbidden = files.filter((file) => forbiddenPatterns.some((pattern) => pattern.test(file)));
|
|
146
131
|
forbidden.length ? fail(`forbidden files in package: ${forbidden.join(", ")}`) : ok("package excludes local env/artifact/debug state");
|
|
147
132
|
}
|
|
148
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
|
+
|
|
149
145
|
function verifyRepoForbiddenFiles() {
|
|
150
|
-
const forbidden =
|
|
151
|
-
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");
|
|
152
148
|
for (const dir of [".artifacts", ".crabbox", ".debug", ".platform-smoke-runs"]) {
|
|
153
|
-
const ignored =
|
|
149
|
+
const ignored = silent("git", ["check-ignore", "-q", `${dir}/probe`]) !== null;
|
|
154
150
|
ignored ? ok(`${dir}/ is gitignored`) : fail(`${dir}/ must be gitignored`);
|
|
155
151
|
}
|
|
156
|
-
const trackedForbidden =
|
|
152
|
+
const trackedForbidden = silent("git", ["ls-files", ".env*", "*.tgz", ".artifacts/*", ".crabbox/*", ".debug/*", ".platform-smoke-runs/*"]);
|
|
157
153
|
trackedForbidden ? fail(`forbidden tracked local state: ${trackedForbidden}`) : ok("no forbidden local state is tracked");
|
|
158
154
|
verifyPackageExclusions();
|
|
159
155
|
}
|
|
160
156
|
|
|
161
157
|
export async function runDoctor(config) {
|
|
162
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");
|
|
163
165
|
console.log("\n── Environment ──");
|
|
164
166
|
const cbox = env("PI_ORACLE_SMOKE_CRABBOX") || env("PLATFORM_SMOKE_CRABBOX") || "crabbox";
|
|
165
167
|
ok(`Crabbox binary = ${resolveCommand(cbox) ?? cbox}${env("PI_ORACLE_SMOKE_CRABBOX") || env("PLATFORM_SMOKE_CRABBOX") ? " (env override)" : " (PATH)"}`);
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
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
|
+
}
|
|
171
181
|
|
|
172
182
|
console.log("\n── Crabbox ──");
|
|
173
183
|
const resolvedCbox = resolveCommand(cbox);
|
|
@@ -181,15 +191,22 @@ export async function runDoctor(config) {
|
|
|
181
191
|
compareVersions(versionLine, requiredVersion) >= 0 ? ok(`required version: ${requiredVersion}+`) : fail(`Crabbox version mismatch: need ${requiredVersion}+, got ${versionLine}`);
|
|
182
192
|
}
|
|
183
193
|
const providers = silent(cbox, ["providers"], { timeout: 30_000 }) ?? "";
|
|
184
|
-
|
|
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) {
|
|
185
200
|
new RegExp(`^${provider}$`, "m").test(providers) ? ok(`provider available: ${provider}`) : fail(`crabbox providers missing ${provider}`);
|
|
186
201
|
}
|
|
187
|
-
runCrabboxDoctor(cbox, "macOS static SSH",
|
|
188
|
-
runCrabboxDoctor(cbox, "Windows native
|
|
189
|
-
runCrabboxDoctor(cbox, "local-container",
|
|
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);
|
|
190
205
|
|
|
191
206
|
console.log("\n── Host tools ──");
|
|
192
|
-
|
|
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) {
|
|
193
210
|
const output = shell(command);
|
|
194
211
|
output ? ok(`${name}: ${output.split("\n")[0]}`) : fail(`${name} not found or unavailable`);
|
|
195
212
|
}
|
|
@@ -198,7 +215,7 @@ export async function runDoctor(config) {
|
|
|
198
215
|
const major = Number(nodeVersion.replace(/^v/, "").split(".")[0]);
|
|
199
216
|
if (major < (config.nodeValidationMajor ?? 24)) fail(`Node ${nodeVersion}; need ${config.nodeValidationMajor ?? 24}+ for smoke validation`);
|
|
200
217
|
}
|
|
201
|
-
verifyWindowsTemplate(config);
|
|
218
|
+
if (hasWindows && !windowsStatic) verifyWindowsTemplate(config);
|
|
202
219
|
|
|
203
220
|
console.log("\n── Auth environment ──");
|
|
204
221
|
const requiredAuth = requiredRealSmokeAuthEnv(config);
|
|
@@ -208,7 +225,7 @@ export async function runDoctor(config) {
|
|
|
208
225
|
}
|
|
209
226
|
|
|
210
227
|
console.log("\n── Target tools ──");
|
|
211
|
-
for (const target of
|
|
228
|
+
for (const target of targets) runTargetToolProbe(cbox, target, config);
|
|
212
229
|
|
|
213
230
|
console.log("\n── Artifact root ──");
|
|
214
231
|
const artifactRoot = resolve(process.cwd(), config.artifactRoot ?? ".artifacts/platform-smoke");
|
|
@@ -232,8 +249,8 @@ export async function runDoctor(config) {
|
|
|
232
249
|
console.log(`\n=== Results: ${failures} failure(s) ===`);
|
|
233
250
|
if (failures > 0) {
|
|
234
251
|
process.exitCode = 1;
|
|
235
|
-
console.log("Fix the failures above before running
|
|
252
|
+
console.log("Fix the failures above before running the selected platform suites.");
|
|
236
253
|
} else {
|
|
237
|
-
console.log(
|
|
254
|
+
console.log(`Ready for platform suites on ${targets.join(", ")}.`);
|
|
238
255
|
}
|
|
239
256
|
}
|