Skip to content

Commit eefc102

Browse files
ShadowfetchShadowfetch
authored andcommitted
Align native qualification with the current first-run workspace
1 parent 83d43ee commit eefc102

7 files changed

Lines changed: 241 additions & 50 deletions

‎README.md‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -214,7 +214,12 @@ assets are checksum-only and cannot be installed by this new entry point; use
214214
the instructions shipped with that released version for those existing assets.
215215
The 0.33.0 release attempt failed before signing and publication. Its tag stays
216216
unchanged. Version 0.33.1 was the first signed release; the public trust policy
217-
in `release/trust` now authorizes 0.33.0 through 2.3.3. An older independently trusted installer bundle whose policy ends at 2.3.1 will reject 2.3.3. Obtain and independently authenticate a reviewed bundle with the updated policy before installing; do not replace trust files with copies from the downloaded release.
217+
in this source checkout's `release/trust` authorizes 0.33.0 through the 2.4.0 candidate;
218+
the latest published release is still 2.3.3. The policy range does not establish
219+
that a candidate has been published or qualified. An older independently trusted
220+
installer bundle whose policy ends at 2.3.1 will reject 2.3.3. Obtain and
221+
independently authenticate a reviewed bundle with the updated policy before
222+
installing; do not replace trust files with copies from the downloaded release.
218223
For development, use [Build from source](#build-from-source).
219224

220225
Obtain an independently authenticated installer bundle as described below,

‎docs/WINDOWS_LAPTOP_QUALIFICATION.md‎

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
# Windows laptop qualification — October 31, 2026
22

3-
Current status: the **public Windows 2.3.1 installer passed** the bounded laptop
4-
qualification. See [the final public-installer record](#231-public-installer-qualification)
5-
for its exact hashes and results. Earlier sections retain the development and
6-
preview history, including failures.
3+
Latest public qualification: **Windows 2.3.3**, recorded in
4+
[the platform qualification report](PLATFORM_QUALIFICATION.md#233-public-release--november-2-2026).
5+
The **2.4.0 source candidate is awaiting installer qualification**.
6+
This document retains the earlier 2.3.1 qualification and development history,
7+
including failures. See [the 2.3.1 public-installer record](#231-public-installer-qualification)
8+
for that version's exact hashes and results.
79

810
Test machine: Windows 11 Home x64, Ryzen 3 5300U, 32 GB RAM. Baseline:
911
`9c82cae76d4c94a4416405c93f00e198080a18ac`, Windows CI preview 2.3.0.

‎docs/WINDOWS_PORT.md‎

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
# Windows port
22

3-
Status: Windows 2.3.1 is published. The exact public installer passed installation,
3+
Status: Windows 2.3.3 is the latest published release. The exact public installer passed installation,
44
clean-PATH startup, native editor/settings checks and a local-model coding task
55
on a Windows 11 Home x64 laptop (Ryzen 3 5300U, 32 GB RAM). Gemma 4 E4B on CPU
66
completed the repair, executed its tests inside AppContainer, and preserved all
77
five original tests; the independent check also passed. This is one bounded
88
fixture, not a broad model-reliability or Windows-hardware benchmark.
9-
See [the laptop qualification record](WINDOWS_LAPTOP_QUALIFICATION.md#231-public-installer-qualification)
10-
for exact hashes, workflow provenance, earlier failed attempts and limitations.
9+
See [the current platform qualification record](PLATFORM_QUALIFICATION.md#233-public-release--november-2-2026)
10+
for the 2.3.3 hashes and results, and [the earlier laptop qualification record](WINDOWS_LAPTOP_QUALIFICATION.md#231-public-installer-qualification)
11+
for the 2.3.1 history, workflow provenance, earlier failed attempts and limitations.
12+
The 2.4.0 workspace upgrade is a source candidate; its installer qualification is
13+
pending and is not covered by those older receipts.
1114

1215
External programs that open the Windows null device themselves can fail in
1316
AppContainer; external Git commands demonstrated this in the 2.3.0 public-installer
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
// Shared by the real native desktop/endurance and opt-in download flows.
2+
// Keep DOM checks callable through WebDriver's execute(script, args).
3+
import assert from "node:assert/strict";
4+
5+
export async function assertFolderOnboarding(execute) {
6+
const dialog = await execute(`
7+
const d = document.querySelector('.wizard[role="dialog"]');
8+
return d && {
9+
text: d.innerText,
10+
fields: [...d.querySelectorAll('input, select, textarea')].map(e => e.type || e.tagName),
11+
open: [...d.querySelectorAll('button')].some(b => b.textContent.trim() === 'Open project'),
12+
folder: !!d.querySelector('#onboarding-folder'),
13+
};
14+
`);
15+
assert.ok(dialog, "Accessible first-run dialog");
16+
assert.match(dialog.text, /Welcome to ShadowCode/);
17+
assert.match(dialog.text, /Ask-first approvals are\s+on/);
18+
assert.match(dialog.text, /Opening trusts this folder: the agent may read it/);
19+
assert.match(dialog.text, /File edits and\s+commands still wait for your approval until you change that/);
20+
assert.doesNotMatch(dialog.text, /\b(Provider|provider|Endpoint|endpoint|API key|Choose a model|Next)\b/, "Folder onboarding has no model step");
21+
assert.deepEqual(dialog.fields, ["text"], "Only a project folder is requested; permissions default to ask-first");
22+
assert.ok(dialog.folder && dialog.open, "Project folder and explicit Open project action");
23+
}
24+
25+
export async function assertWorkspacePermissions(api, project) {
26+
const status = await api("GET", "/api/workspace/status");
27+
assert.equal(status.workspace, project);
28+
assert.equal(status.trusted, true, "Opening explicitly trusts the chosen workspace");
29+
assert.equal(status.onboarding?.completed, true, "Onboarding completion persisted");
30+
assert.equal(status.onboarding?.workspace, project, "Onboarding records the chosen folder");
31+
assert.equal(status.permissions?.mode, "ask", "Live workspace still requires approvals");
32+
assert.equal(status.permissions?.level, "workspace", "Opening does not grant elevated access");
33+
const config = await api("GET", "/api/config");
34+
const values = config.values || config.config || config;
35+
assert.equal(values.permissions?.mode, "ask", "Ask-first mode saved in configuration");
36+
assert.equal(values.permissions?.level, "workspace", "Workspace permission level saved");
37+
}
38+
39+
export async function assertWelcomeControls(execute) {
40+
// The inline API-key provider is a legitimate select, not a model picker.
41+
const unexpected = await execute(`return [...document.querySelectorAll('select')]
42+
.filter(s => !s.closest('.worktree-base') && !s.matches('.welcome-model-setup .api-key-quick select[aria-label="API key provider"]'))
43+
.map(s => s.outerHTML)`);
44+
assert.deepEqual(unexpected, [], "No select-based model controls in the workspace");
45+
}
46+
47+
export async function assertWelcomeModelSetup(execute) {
48+
const setup = await execute(`
49+
const s = document.querySelector('.welcome-model-setup .model-setup');
50+
if (!s) return null;
51+
const key = s.querySelector('input[aria-label="OpenRouter API key"]');
52+
return {
53+
radios: [...s.querySelectorAll('.mode-options input[type=radio]')].map(r => r.closest('label').innerText),
54+
providers: [...s.querySelectorAll('.provider-tile button[aria-label]')].map(b => b.getAttribute('aria-label')),
55+
keyType: key?.type,
56+
keyProviders: [...s.querySelectorAll('.api-key-quick select option')].map(o => o.value),
57+
saveDisabled: s.querySelector('.api-key-quick button[type=submit]')?.disabled,
58+
};
59+
`);
60+
assert.ok(setup, "Model setup is in the empty conversation after onboarding");
61+
assert.equal(setup.radios.length, 1, "Local download is the single model-setup radio");
62+
assert.match(setup.radios[0], /Download a free model to run on this computer/);
63+
// A real CI host may lack vendor CLIs: honest 'not installed' controls are
64+
// valid, as are Connect and Connected. Never start a vendor login here.
65+
for (const provider of ["Codex", "Claude Code", "Cursor", "Grok"]) {
66+
assert.ok(setup.providers.some(label => [
67+
`Connect ${provider}`, `Connected ${provider}`, `${provider} is not installed`,
68+
].includes(label)), `${provider} connection or availability control`);
69+
}
70+
assert.ok(setup.providers.some(label => /Antigravity/.test(label)), "Antigravity connection or install status");
71+
assert.equal(setup.keyType, "password", "OpenRouter key entry is masked");
72+
assert.deepEqual(setup.keyProviders, ["openrouter", "grok", "openai"], "Inline key provider choices");
73+
assert.equal(setup.saveDisabled, true, "An empty API key cannot be saved");
74+
}

‎scripts/test-native-desktop.mjs‎

Lines changed: 10 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
// SHADOW_EXPECT_VENDORS e.g. "codex=Ready,claude=Sign in": exact picker
3030
// availability expected for vendors on this machine
3131
import assert from "node:assert/strict";
32+
import { assertFolderOnboarding, assertWorkspacePermissions, assertWelcomeModelSetup, assertWelcomeControls } from "./fixtures/native-onboarding-smoke.mjs";
3233
import { capturePrivateSession, writePrivateSessionReport } from "./native-test-session.mjs";
3334
import { EDITOR_FILE, OTHER_FILE, seedNativeEditorFixtures, nativeEditorBeforeReload, nativeEditorAfterReload } from "./fixtures/native-editor-smoke.mjs";
3435
import { nativeRunCheck } from "./fixtures/native-run-check-smoke.mjs";
@@ -489,34 +490,15 @@ try {
489490
}
490491
const expectedScript = (await readFile(path.join(root, "ui/dist/index.html"), "utf8")).match(/src="([^"]+\.js)"/)[1];
491492
assert.equal(await execute("return new URL(document.querySelector('script[type=module]').src).pathname"), expectedScript, "The binary embeds the current ui/dist build");
492-
const onboarding = await text();
493-
assert.match(onboarding, /Welcome to ShadowCode/);
494-
assert.match(onboarding, /Ask before actions/); assert.match(onboarding, /Allow project edits/);
495-
assert.doesNotMatch(onboarding, /\b(Provider|provider|Endpoint|endpoint|API key|Choose a model|Next)\b/, "Onboarding has no model step");
496-
assert.deepEqual(await execute("return [...document.querySelectorAll('.wizard input, .wizard select, .wizard textarea')].map(e=>e.type||e.tagName)"), ["text", "radio", "radio"], "Onboarding asks only for the folder and the permission mode");
497-
assert.equal(await execute("return document.querySelector('input[name=onboarding-mode]:checked').closest('label').textContent.includes('Ask before actions')"), true, "Ask before actions is the default");
493+
await assertFolderOnboarding(execute);
498494
await fill("#onboarding-folder", project);
499495
await setTheme("light");
500496
await screenshot("onboarding");
501497
await accessibility("onboarding");
502-
await clickButton("Trust and open");
503-
// Without any ready model (no signed-in subscription on this machine), the
504-
// first run asks how to get one; this run skips it (test-native-first-run.mjs
505-
// covers that step).
506-
const modelStep = await until("Onboarding closes or asks for a model", () => execute("if(!document.querySelector('.wizard'))return 'closed';return /Choose how ShadowCode thinks/.test(document.body.innerText)?'model':false"), 20000);
507-
if (modelStep === "model") {
508-
const choices = await text();
509-
for (const choice of [/Download a free model to run on this computer/, /Use an OpenRouter key/, /Sign in to a subscription/]) assert.match(choices, choice);
510-
await clickButton("Skip for now");
511-
note("first run without a ready model offers download, OpenRouter and subscription; skipped here");
512-
}
498+
await clickButton("Open project");
513499
await until("Workspace ready", () => execute("const t=document.querySelector(arguments[0]);return !!t && !t.disabled && !document.querySelector('.wizard')", [composer]), 20000);
514-
const status = await api("GET", "/api/workspace/status");
515-
assert.equal(status.workspace, project);
516-
const config = await api("GET", "/api/config");
517-
const cfgValues = config.values || config.config || config;
518-
assert.equal(cfgValues.permissions?.mode, "ask", "Onboarding saved Ask before actions");
519-
note("first run: folder, trust and permission mode (Ask before actions), no model step");
500+
await assertWorkspacePermissions(api, project);
501+
note("first run: Open project trusts the folder, saves ask-first workspace permissions and opens the composer directly");
520502
// Onboarding saves "system"; the screenshots below choose explicitly.
521503
await setTheme("light");
522504

@@ -528,8 +510,7 @@ try {
528510
});
529511
assert.ok(suggestions.length <= 3, `At most three suggestions (${suggestions})`);
530512
assert.equal(await execute("return document.querySelectorAll('.unified-picker-trigger').length"), 1, "Exactly one model picker");
531-
// More's "New worktree starts from" branch list is the one select allowed.
532-
assert.equal(await execute("return [...document.querySelectorAll('select')].filter(s=>!s.closest('.worktree-base')).length"), 0, "No select-based model controls in the workspace");
513+
await assertWelcomeControls(execute);
533514
assert.ok(await visible('button[aria-label^="Attach"]'), "Attachment button");
534515
await screenshot("welcome-light");
535516
await accessibility("welcome-light");
@@ -555,19 +536,16 @@ try {
555536
await until("Welcome model action", () => visible(".welcome-model-setup button"));
556537
await screenshot("welcome-model-ready");
557538
// With a ready model (a signed-in vendor CLI on this machine) the welcome
558-
// offers "Choose a model"; with none (a clean CI host) it offers a free
559-
// download, an OpenRouter key or a subscription instead.
539+
// offers "Choose a model"; with none (a clean CI host) the conversation
540+
// offers vendor connection controls, an API key form and a free download.
560541
const canChoose = await execute("return [...document.querySelectorAll('.welcome-model-setup button')].some(b => b.textContent.trim() === 'Choose a model' && b.getClientRects().length > 0)");
561542
if (canChoose) {
562543
await clickButton("Choose a model", "//div[contains(@class,'welcome-model-setup')]");
563544
await until("Welcome action opens picker", () => visible(".unified-picker-menu"));
564545
note("first-run Choose a model opens the unified picker after model discovery");
565546
} else {
566-
const setup = await execute("return document.querySelector('.welcome-model-setup').innerText");
567-
assert.match(setup, /free model|Download/i, "No-model welcome offers a free model");
568-
assert.match(setup, /OpenRouter/i, "No-model welcome offers an OpenRouter key");
569-
assert.match(setup, /subscription/i, "No-model welcome offers a subscription");
570-
note("no model ready: the welcome offers a free model, an OpenRouter key or a subscription");
547+
await assertWelcomeModelSetup(execute);
548+
note("no model ready: inline vendor connection/status controls, API key form and local download");
571549
}
572550
await until("Vendor rows probed", async () => {
573551
const data = await api("GET", "/api/picker");

‎scripts/test-native-first-run.mjs‎

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22
// no vendor CLI on PATH, an empty HOME, no API keys, no local model. The real
33
// llama.cpp runtime (the managed install) detects this computer's hardware.
44
//
5-
// The run: onboarding → the model step (three choices; the recommendation
6-
// comes from this computer) → "See other free models" → download one model
5+
// The run: folder onboarding → the empty conversation (provider connections,
6+
// API key form and hardware-based local recommendation) → "See other free models" → download one model
77
// for real from Hugging Face with Pause and Resume → it is selected in the
88
// composer → one short read-only task on it → Delete → quit. Nothing is
99
// downloaded before the click. The profile (and the ~2 GB download) lives in
@@ -19,6 +19,7 @@
1919
// SHADOW_FIRST_RUN_MODEL catalog id to download (default granite-4.2-3b, the smallest)
2020
// SHADOW_KEEP_SCRATCH=1 keep the scratch profile
2121
import assert from "node:assert/strict";
22+
import { assertFolderOnboarding, assertWorkspacePermissions, assertWelcomeModelSetup, assertWelcomeControls } from "./fixtures/native-onboarding-smoke.mjs";
2223
import { capturePrivateSession, writePrivateSessionReport } from "./native-test-session.mjs";
2324
import { createServer } from "node:http";
2425
import { spawn, execFile } from "node:child_process";
@@ -221,28 +222,34 @@ try {
221222
assert.ok(target, `${modelId} is in the download list`);
222223
const recommended = catalog.models.find((m) => m.id === catalog.recommended);
223224
note(`hardware: ${catalog.hardware.gpu || "no GPU"} (${Math.round((catalog.hardware.vram_bytes || 0) / 2 ** 30)} GB) · ${Math.round(catalog.hardware.ram_bytes / 2 ** 30)} GB RAM → recommended ${recommended?.name || "none"} (${catalog.recommended_fit || "-"})`);
225+
await assertFolderOnboarding(execute);
224226
await fill("#onboarding-folder", project);
225-
await clickButton("Trust and open");
227+
await clickButton("Open project");
228+
await until("Workspace ready", () => execute("const t=document.querySelector('textarea[aria-label=\"Message ShadowCode\"]');return !!t && !t.disabled && !document.querySelector('.wizard')"), 30000);
229+
await assertWorkspacePermissions(api, project);
226230

227-
// ------------------------------------------------------------ step 2
228-
await until("Model step", async () => /Choose how ShadowCode thinks/.test(await text()), 30000);
229-
const step = await text();
230-
for (const choice of [/Download a free model to run on this computer/, /Use an OpenRouter key/, /Sign in to a subscription/]) assert.match(step, choice);
231+
// ------------------------------------------------------------ empty conversation
232+
await until("Inline model setup", () => visible(".welcome-model-setup .model-setup"), 30000);
233+
await assertWelcomeModelSetup(execute);
234+
await assertWelcomeControls(execute);
235+
// Catalog rendering is asynchronous even when the API above has returned.
236+
await until("Local recommendation settled", () => execute("const e=document.querySelector('.welcome-model-setup .model-setup-detail');return !!e && !/Checking what this computer can run/.test(e.innerText)"), 30000);
237+
const step = await execute("return document.querySelector('.welcome-model-setup').innerText");
231238
if (recommended) {
232239
assert.match(step, new RegExp(`${recommended.name} by ${recommended.publisher}: [\\d.]+ GB, about`), "Recommended model with size and time");
233240
assert.match(step, /Picked for this computer: /);
234-
assert.equal(await execute("const r=document.querySelector('.wizard input[type=radio]:checked');return r?r.closest('label').innerText:''").then((t) => /Download a free model/.test(t)), true, "Download is preselected");
241+
assert.equal(await execute("const r=document.querySelector('.welcome-model-setup .mode-options input[type=radio]:checked');return r?r.closest('label').innerText:''").then((t) => /Download a free model/.test(t)), true, "Download is preselected");
235242
}
236243
const picker = await api("GET", "/api/picker?cached=1");
237244
assert.ok(!picker.targets.some((t) => t.availability === "ready"), "No model is ready on a first run without accounts");
238245
assert.equal(existsSync(downloads) ? (await readdir(downloads)).length : 0, 0, "Nothing is downloaded before a click");
239246
await screenshot("first-run-model-step");
240247
await accessibility("first-run-model-step");
241-
note(`first run without accounts: model step with three choices${recommended ? `, ${recommended.name} preselected` : ""}; nothing downloaded`);
248+
note(`first run without accounts: inline provider connections, API key form and local download${recommended ? `, ${recommended.name} preselected` : ""}; nothing downloaded`);
242249

243250
// ------------------------------------------------------------ download
244251
await clickButton("See other free models");
245-
await until("Settings › Local models", async () => /Download a free model/.test(await text()), 20000);
252+
await until("Settings › Local models", () => execute("const s=document.querySelector('.settings-body');return !!s?.querySelector('#local-gguf') && /Download a free model/i.test(s.innerText)"), 20000);
246253
const sizeLabel = await until(`${target.name} offers Download`, async () => (await rowText(target.name)).match(/Download \(([^)]+)\)/)?.[1], 20000);
247254
await screenshot("local-models-before");
248255
await accessibility("local-models");

0 commit comments

Comments
 (0)