Skip to content

Commit 6e71534

Browse files
authored
[codex] fix Codex 26.727 Settings renderer detection (#334)
* fix Codex 26.727 settings renderer detection * test: make bootstrap contract line-ending independent * docs: record PR 334 CI follow-up
1 parent 4b4ebfa commit 6e71534

14 files changed

Lines changed: 188 additions & 25 deletions

TASK_PROGRESS.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,31 @@
11
# Task Progress
22

3+
## PR #334 Windows CRLF CI follow-up — locally verified (2026-08-01)
4+
5+
- [scope] Client worktree `/private/tmp/dreamskin-settings-fix.4tH4ld` is on
6+
`codex/fix-26-727-settings-renderer`; PR #334 targets `main`. The pre-fix PR
7+
head is `159c650c3b43877df4413a7b0a20562fe556a018`.
8+
- [reproduced from CI] Run `30646840223` passed Static checks and macOS
9+
repository regressions, but both Windows PowerShell 5.1 and PowerShell 7 jobs
10+
failed in their regression suite. On the Windows CRLF checkout, the bootstrap
11+
source-contract test's fixed 2,200-character slice ended at
12+
`setInterval(in`, before the asserted `setInterval(install, 250)` text.
13+
- [fixed locally/tests only] Both platform bootstrap tests now inspect the
14+
complete string returned by the already imported
15+
`earlyPayloadFor("", "source-contract")`. This removes line-ending-dependent
16+
truncation without changing or weakening the early-injection assertions and
17+
without changing runtime implementation.
18+
- [verified locally] `node macos/tests/injector-bootstrap.test.mjs`,
19+
`node windows/tests/injector-bootstrap.test.mjs`, both platform
20+
`renderer-inject.test.mjs` tests, `node tools/doctor-selectors.test.mjs`, and
21+
`git diff --check` all pass.
22+
- [committed/pushed] Test-only fix commit
23+
`7d19780ec56446c3d1bd1ac61931588c5487f55f` is pushed to
24+
`origin/codex/fix-26-727-settings-renderer` for PR #334.
25+
- [pending] Require a fresh exact-head CI pass for all four jobs before merge.
26+
No merge, release, deployment, Issue reply, or Issue closure has occurred in
27+
this follow-up.
28+
329
## Client release v1.5.10 — in progress (2026-07-31)
430

531
- [base] Feature PR #324 was squash-merged to `main` at

macos/assets/renderer-inject.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
// Canonical cross-platform renderer. Run tools/sync-runtime-assets.mjs after editing.
22
((cssText, artDataUrl, themeConfig) => {
3-
const SELECTOR_CONTRACT = {"schema":"codex-dream-skin-selectors/1","selectors":[{"key":"shell-main","selector":"main:is(.main-surface, [data-app-shell-main-surface], [class*=\"_MainContentSurface_\"])","tier":"L1","scope":"all","required":true},{"key":"left-panel","selector":"aside.app-shell-left-panel","tier":"L1","scope":"all","required":true},{"key":"header-tint","selector":"header:is(.app-header-tint, [data-app-shell-header-edge-scroll], [class*=\"_Header_\"])","tier":"L1","scope":"all","required":true},{"key":"main-content-top-fade","selector":":is(.app-shell-main-content-top-fade, [data-app-shell-main-content-top-fade], [class*=\"_MainContentTopFade_\"])","tier":"L2","scope":"all","required":false},{"key":"home-icon","selector":"[data-testid=\"home-icon\"]","tier":"L1","scope":"home","required":true},{"key":"home-route","selector":"[role=\"main\"]:has([data-testid=\"home-icon\"])","tier":"L1","scope":"home","required":true},{"key":"home-route-css","selector":"[role=\"main\"]","tier":"L1","scope":"home","required":true},{"key":"home-banners","selector":".home-banners","tier":"L2","scope":"home","required":false},{"key":"composer-chrome","selector":".composer-surface-chrome","tier":"L2","scope":"home+thread","required":false},{"key":"composer-toolbar","selector":".composer-surface-chrome [class*=\"_footer_\"]","tier":"L2","scope":"home+thread","required":false},{"key":"home-utility","selector":"[class*=\"_homeUtilityBar_\"]","tier":"L2","scope":"home","required":false},{"key":"game-source","selector":"[data-feature=\"game-source\"]","tier":"L2","scope":"home","required":false},{"key":"home-suggestions","selector":".group\\/home-suggestions","tier":"L2","scope":"home","required":false},{"key":"project-selector","selector":".group\\/project-selector","tier":"L2","scope":"home config","required":false},{"key":"markdown","selector":"[class*=\"_markdown\"]","tier":"L2","scope":"thread","required":false},{"key":"thread-surface","selector":".thread-scroll-container","tier":"L2","scope":"thread","required":false},{"key":"message","selector":":is([data-message-author-role], [data-local-conversation-user-anchor], [data-local-conversation-final-assistant])","tier":"L2","scope":"thread","required":false},{"key":"appearance-radio","selector":"input[name=\"appearance-theme\"]","tier":"L2","scope":"settings","required":false},{"key":"overlay-menu","selector":"[role=\"menu\"]","tier":"L2","scope":"overlay","required":false},{"key":"overlay-dialog","selector":"[role=\"dialog\"]","tier":"L2","scope":"overlay","required":false},{"key":"overlay-popper","selector":"[data-radix-popper-content-wrapper]","tier":"L2","scope":"overlay","required":false}],"stableTestids":["app-shell-header-context-menu-surface","home-icon","theme-preview"]};
3+
const SELECTOR_CONTRACT = {"schema":"codex-dream-skin-selectors/1","selectors":[{"key":"shell-main","selector":"main:is(.main-surface, [data-app-shell-main-surface], [class*=\"_MainContentSurface_\"])","tier":"L1","scope":"all","required":true},{"key":"left-panel","selector":"aside.app-shell-left-panel","tier":"L1","scope":"all","required":true},{"key":"header-tint","selector":"header:is(.app-header-tint, [data-app-shell-header-edge-scroll], [class*=\"_Header_\"])","tier":"L1","scope":"all","required":true},{"key":"main-content-top-fade","selector":":is(.app-shell-main-content-top-fade, [data-app-shell-main-content-top-fade], [class*=\"_MainContentTopFade_\"])","tier":"L2","scope":"all","required":false},{"key":"home-icon","selector":"[data-testid=\"home-icon\"]","tier":"L1","scope":"home","required":true},{"key":"home-route","selector":"[role=\"main\"]:has([data-testid=\"home-icon\"])","tier":"L1","scope":"home","required":true},{"key":"home-route-css","selector":"[role=\"main\"]","tier":"L1","scope":"home","required":true},{"key":"home-banners","selector":".home-banners","tier":"L2","scope":"home","required":false},{"key":"composer-chrome","selector":".composer-surface-chrome","tier":"L2","scope":"home+thread","required":false},{"key":"composer-toolbar","selector":".composer-surface-chrome [class*=\"_footer_\"]","tier":"L2","scope":"home+thread","required":false},{"key":"home-utility","selector":"[class*=\"_homeUtilityBar_\"]","tier":"L2","scope":"home","required":false},{"key":"game-source","selector":"[data-feature=\"game-source\"]","tier":"L2","scope":"home","required":false},{"key":"home-suggestions","selector":".group\\/home-suggestions","tier":"L2","scope":"home","required":false},{"key":"project-selector","selector":".group\\/project-selector","tier":"L2","scope":"home config","required":false},{"key":"markdown","selector":"[class*=\"_markdown\"]","tier":"L2","scope":"thread","required":false},{"key":"thread-surface","selector":".thread-scroll-container","tier":"L2","scope":"thread","required":false},{"key":"message","selector":":is([data-message-author-role], [data-local-conversation-user-anchor], [data-local-conversation-final-assistant])","tier":"L2","scope":"thread","required":false},{"key":"settings-panel","selector":"[data-settings-panel-slug=\"general-settings\"]","tier":"L2","scope":"settings","required":false},{"key":"appearance-radio","selector":"input[name=\"appearance-theme\"]","tier":"L2","scope":"settings","required":false},{"key":"overlay-menu","selector":"[role=\"menu\"]","tier":"L2","scope":"overlay","required":false},{"key":"overlay-dialog","selector":"[role=\"dialog\"]","tier":"L2","scope":"overlay","required":false},{"key":"overlay-popper","selector":"[data-radix-popper-content-wrapper]","tier":"L2","scope":"overlay","required":false}],"stableTestids":["app-shell-header-context-menu-surface","home-icon","theme-preview"]};
44
const STATE_KEY = "__CODEX_DREAM_SKIN_STATE__";
55
const DISABLED_KEY = "__CODEX_DREAM_SKIN_DISABLED__";
66
const STYLE_REGISTRY_KEY = "__CODEX_DREAM_SKIN_STYLE_SHEETS__";
@@ -678,7 +678,8 @@
678678
const overlay = selectorHit("overlay-menu") || selectorHit("overlay-dialog") ||
679679
selectorHit("overlay-popper");
680680
let baseState = "thread";
681-
if (selectorHit("appearance-radio") || stableTestidHit("theme-preview")) baseState = "settings";
681+
if (selectorHit("settings-panel") || selectorHit("appearance-radio") ||
682+
stableTestidHit("theme-preview")) baseState = "settings";
682683
else if (selectorHit("home-icon") || selectorHit("home-route")) baseState = "home";
683684
else if (!selectorHit("shell-main") && !document.querySelector('main, [role="main"]')) baseState = "settings";
684685
const missingL1 = SELECTOR_CONTRACT.selectors

macos/assets/selectors.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,8 @@
4444
{ "key": "markdown", "selector": "[class*=\"_markdown\"]", "tier": "L2", "scope": "thread", "required": false, "notes": "会话内容块,单页可达 100+ 个,样式规则注意性能" },
4545
{ "key": "thread-surface", "selector": ".thread-scroll-container", "tier": "L2", "scope": "thread", "required": false, "notes": "会话滚动表面;双端活动会话快照均存在" },
4646
{ "key": "message", "selector": ":is([data-message-author-role], [data-local-conversation-user-anchor], [data-local-conversation-final-assistant])", "tier": "L2", "scope": "thread", "required": false, "notes": "用户与助手消息的稳定语义边界;兼容旧版 role 属性和 Codex 26.727.4816 的用户/助手语义属性,运行时对子节点变更做合并刷新" },
47-
{ "key": "appearance-radio", "selector": "input[name=\"appearance-theme\"]", "tier": "L2", "scope": "settings", "required": false, "notes": "外观三选一;theme-preview testid 同屏出现" },
47+
{ "key": "settings-panel", "selector": "[data-settings-panel-slug=\"general-settings\"]", "tier": "L2", "scope": "settings", "required": false, "notes": "Codex 26.727 设置页稳定导航锚点;设置页可先落在 General,不再保证 appearance-theme 或 theme-preview 存在" },
48+
{ "key": "appearance-radio", "selector": "input[name=\"appearance-theme\"]", "tier": "L2", "scope": "settings", "required": false, "notes": "旧版外观三选一;新设置页可能仅有 settings-panel" },
4849
{ "key": "overlay-menu", "selector": "[role=\"menu\"]", "tier": "L2", "scope": "overlay", "required": false },
4950
{ "key": "overlay-dialog", "selector": "[role=\"dialog\"]", "tier": "L2", "scope": "overlay", "required": false },
5051
{ "key": "overlay-popper", "selector": "[data-radix-popper-content-wrapper]", "tier": "L2", "scope": "overlay", "required": false }

macos/scripts/injector.mjs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -522,7 +522,8 @@ async function probeSession(session) {
522522
main: Boolean(document.querySelector(${selectorLiteral("home-route")})),
523523
generic: genericCodexSurface(),
524524
};
525-
const settings = Boolean(document.querySelector(${selectorLiteral("appearance-radio")})) ||
525+
const settings = Boolean(document.querySelector(${selectorLiteral("settings-panel")})) ||
526+
Boolean(document.querySelector(${selectorLiteral("appearance-radio")})) ||
526527
Boolean(document.querySelector(${stableTestidLiteral("theme-preview")}));
527528
return {
528529
markers,
@@ -1154,6 +1155,7 @@ async function verifySession(session, expectedThemeId = null, expectedRevision =
11541155
const genericMain = box(document.querySelector('[data-ds-part="main"], [data-ds-part="home"]'));
11551156
const genericInput = box(document.querySelector('[data-ds-part="composer"]'));
11561157
const settingsBoxes = [
1158+
box(document.querySelector(${selectorLiteral("settings-panel")})),
11571159
box(document.querySelector(${selectorLiteral("appearance-radio")})),
11581160
box(document.querySelector(${stableTestidLiteral("theme-preview")})),
11591161
];
@@ -1406,7 +1408,8 @@ export function earlyPayloadFor(payload, revision) {
14061408
const shell = document.querySelector(${selectorLiteral("shell-main")});
14071409
const sidebar = document.querySelector(${selectorLiteral("left-panel")});
14081410
const main = document.querySelector(${selectorLiteral("home-route")});
1409-
const settings = document.querySelector(${selectorLiteral("appearance-radio")}) ||
1411+
const settings = document.querySelector(${selectorLiteral("settings-panel")}) ||
1412+
document.querySelector(${selectorLiteral("appearance-radio")}) ||
14101413
document.querySelector(${stableTestidLiteral("theme-preview")});
14111414
const genericMain = document.querySelector('main, [role="main"]');
14121415
const genericInput = document.querySelector('textarea, [contenteditable="true"], [role="textbox"]');

macos/tests/injector-bootstrap.test.mjs

Lines changed: 55 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ function createFixture() {
2020
shell: false,
2121
sidebar: false,
2222
main: false,
23+
settingsPanel: false,
2324
settings: false,
2425
genericInput: false,
2526
branding: false,
@@ -36,6 +37,9 @@ function createFixture() {
3637
if (selector === "aside.app-shell-left-panel") return markers.sidebar ? {} : null;
3738
if (selector === "[role=\"main\"]") return markers.main ? {} : null;
3839
if (selector === "main, [role=\"main\"]") return markers.main ? {} : null;
40+
if (selector === '[data-settings-panel-slug="general-settings"]') {
41+
return markers.settingsPanel ? {} : null;
42+
}
3943
if (selector.includes("textarea") || selector.includes("contenteditable") || selector.includes("textbox")) {
4044
return markers.genericInput ? {} : null;
4145
}
@@ -96,6 +100,16 @@ generic.tick();
96100
assert.deepEqual(generic.context.window.installs, ["generic"],
97101
"A verified app:// Codex surface with generic main/input anchors must accept newer renderer shells.");
98102

103+
const settingsPanel = createFixture();
104+
vm.runInNewContext(
105+
earlyPayloadFor('window.installs.push("settings-panel")', "settings-panel"),
106+
settingsPanel.context,
107+
);
108+
settingsPanel.markers.settingsPanel = true;
109+
settingsPanel.tick();
110+
assert.deepEqual(settingsPanel.context.window.installs, ["settings-panel"],
111+
"Codex 26.727 Settings must accept its stable general-settings panel without legacy appearance controls.");
112+
99113
const generations = createFixture();
100114
generations.makeNotReady();
101115
generations.markers.shell = true;
@@ -111,9 +125,7 @@ assert.deepEqual(
111125
);
112126
assert.equal(generations.context.window.__CODEX_DREAM_SKIN_EARLY_APPLIED__, "new");
113127

114-
const earlyStart = source.indexOf("export function earlyPayloadFor");
115-
const earlySource = source.slice(earlyStart, earlyStart + 2200);
116-
assert.ok(earlyStart >= 0, "Early payload helper must remain exported for bootstrap tests.");
128+
const earlySource = earlyPayloadFor("", "source-contract");
117129
assert.doesNotMatch(earlySource, /MutationObserver|childList|subtree/,
118130
"Early bootstrap must not observe the entire renderer DOM.");
119131
assert.doesNotMatch(earlySource, /document\.title|document\.body\?\.innerText|location\.href/,
@@ -123,6 +135,46 @@ assert.match(earlySource, /setInterval\(install, 250\)/);
123135
const identityProbeStart = source.indexOf("async function probeSession");
124136
const identityProbeSource = source.slice(identityProbeStart, identityProbeStart + 1800);
125137
assert.ok(identityProbeStart >= 0, "The live target probe must remain covered by the identity test.");
138+
const probePrefix = "return session.evaluate(`";
139+
const probePayloadStart = source.indexOf(probePrefix, identityProbeStart) + probePrefix.length;
140+
const probePayloadEnd = source.indexOf("`);", probePayloadStart);
141+
assert.ok(probePayloadStart >= probePrefix.length && probePayloadEnd > probePayloadStart,
142+
"The live identity expression must remain extractable for behavioral testing.");
143+
const probeTemplate = source.slice(probePayloadStart, probePayloadEnd);
144+
assert.doesNotMatch(probeTemplate, /`/, "The live identity expression must not contain nested template literals.");
145+
const liveProbePayload = vm.runInNewContext(`\`${probeTemplate}\``, {
146+
selectorLiteral: (key) => JSON.stringify(`[selector-${key}]`),
147+
stableTestidLiteral: (key) => JSON.stringify(`[data-testid="${key}"]`),
148+
});
149+
const runLiveProbe = ({
150+
protocol = "app:", settingsPanel: hasSettingsPanel = false,
151+
genericMain = false, genericInput = false, branding = false,
152+
} = {}) => vm.runInNewContext(liveProbePayload, {
153+
location: { protocol },
154+
document: {
155+
querySelector(selector) {
156+
if (selector === "[selector-settings-panel]") return hasSettingsPanel ? {} : null;
157+
if (selector === 'main, [role="main"]') return genericMain ? {} : null;
158+
if (selector === 'textarea, [contenteditable="true"], [role="textbox"]') {
159+
return genericInput ? {} : null;
160+
}
161+
if (selector === '[data-testid="app-shell-header-context-menu-surface"]') {
162+
return branding ? {} : null;
163+
}
164+
return null;
165+
},
166+
},
167+
});
168+
assert.equal(runLiveProbe({ settingsPanel: true }).codex, true,
169+
"The live probe must accept the Codex 26.727 general Settings panel on app://.");
170+
assert.equal(runLiveProbe({ protocol: "https:", settingsPanel: true }).codex, false,
171+
"The Settings marker must never identify a non-app target.");
172+
assert.equal(runLiveProbe({ genericMain: true, genericInput: true }).codex, false,
173+
"The live probe must reject an unbranded generic app target.");
174+
assert.equal(runLiveProbe({ genericMain: true, genericInput: true, branding: true }).codex, true,
175+
"The live probe may accept generic anchors only with the stable Codex branding marker.");
176+
assert.match(identityProbeSource, /selectorLiteral\("settings-panel"\)/,
177+
"The live probe must retain the current Settings structural marker.");
126178
assert.match(identityProbeSource, /return Boolean\(main && input && branded\)/,
127179
"The live target probe must require branding together with both generic anchors.");
128180
assert.match(identityProbeSource, /app-shell-header-context-menu-surface/,

runtime/renderer-inject.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -678,7 +678,8 @@
678678
const overlay = selectorHit("overlay-menu") || selectorHit("overlay-dialog") ||
679679
selectorHit("overlay-popper");
680680
let baseState = "thread";
681-
if (selectorHit("appearance-radio") || stableTestidHit("theme-preview")) baseState = "settings";
681+
if (selectorHit("settings-panel") || selectorHit("appearance-radio") ||
682+
stableTestidHit("theme-preview")) baseState = "settings";
682683
else if (selectorHit("home-icon") || selectorHit("home-route")) baseState = "home";
683684
else if (!selectorHit("shell-main") && !document.querySelector('main, [role="main"]')) baseState = "settings";
684685
const missingL1 = SELECTOR_CONTRACT.selectors

tools/doctor-selectors.mjs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,8 @@ function pageDoctor(selectors, stableTestids = []) {
9595
const overlay = count("overlay-menu") > 0 || count("overlay-dialog") > 0 ||
9696
count("overlay-popper") > 0;
9797
let baseState = "thread";
98-
if (count("appearance-radio") > 0 || stableTestidCount("theme-preview") > 0) baseState = "settings";
98+
if (count("settings-panel") > 0 || count("appearance-radio") > 0 ||
99+
stableTestidCount("theme-preview") > 0) baseState = "settings";
99100
else if (count("home-icon") > 0 || count("home-route") > 0) baseState = "home";
100101
else if (count("shell-main") === 0) baseState = "settings";
101102

tools/doctor-selectors.test.mjs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,11 @@ assert.equal(
2727
':is([data-message-author-role], [data-local-conversation-user-anchor], [data-local-conversation-final-assistant])',
2828
"The message contract must bridge both legacy and Codex 26.727 role boundaries.",
2929
);
30+
assert.equal(
31+
selectorFor("settings-panel"),
32+
'[data-settings-panel-slug="general-settings"]',
33+
"The Settings contract must use the stable Codex 26.727 general-settings panel marker.",
34+
);
3035
const resultFor = (baseState, hits, overlay = false) => gradeDoctorResult(contract, {
3136
baseState,
3237
overlay,
@@ -46,10 +51,12 @@ const brokenHome = resultFor("home", ["shell-main", "left-panel", "header-tint",
4651
assert.equal(brokenHome.pass, false);
4752
assert.equal(brokenHome.exitCode, 1);
4853

49-
const settings = resultFor("settings", ["appearance-radio"]);
54+
const settings = resultFor("settings", ["settings-panel"]);
5055
assert.equal(settings.pass, true);
5156
assert.equal(settings.tiers.L1.length, 0, "Settings must not inherit home/all L1 requirements");
52-
assert.deepEqual(settings.tiers.L2.map(({ key }) => key), ["appearance-radio"]);
57+
assert.deepEqual(settings.tiers.L2.map(({ key }) => key), ["settings-panel", "appearance-radio"]);
58+
assert.equal(settings.tiers.L2.find(({ key }) => key === "settings-panel").status, "ok");
59+
assert.equal(settings.tiers.L2.find(({ key }) => key === "appearance-radio").status, "miss");
5360

5461
assert.equal(selectorMatchesScope("home+thread", { baseState: "thread", overlay: false }), true);
5562
assert.equal(selectorMatchesScope("home config", { baseState: "home", overlay: false }), true);

0 commit comments

Comments
 (0)