mirror of
https://github.com/Heretek-AI/GDevelop-Unlocked.git
synced 2026-11-07 10:55:58 -10:00
fix(ai-studio): resolve undefined parent global that broke release builds
customCreateSubAgentAiRequest computed hasOpenProject with a bare `parent` identifier that does not exist in scope (it resolved to the `window.parent` global), so CRA's no-restricted-globals rule failed `npm run build` on all three OS jobs of release run 38344881723. Fall back to the parent request in localAiRequestsCache[parentAiRequestId], mirroring the continue-turn path's `existing.gameProjectJson`. Add tests/unit/tier2-restricted-globals.test.mjs: a targeted guard for this regression class, which `npm test` could not see because it never runs eslint. Verified red on the pre-fix source (reports 5182:27 / 5182:37, exactly matching the CI eslint output) and green after the fix. Refresh the MANIFEST.json pin for CustomAIClient.js (219717 bytes). Suite: 241/241. Seam gate green. Patched-tree apply verified on a scratch copy.
This commit is contained in:
1 parent
7586180f5d
commit
26f6c869d8
3 files changed
+154
-3
No files matched your search
@@ -5178,8 +5178,13 @@ export const customCreateSubAgentAiRequest = async ({
|
||||
// read-only subset instead of failing open, which is why this differs from
|
||||
// the continue-turn fallback above (there the role only re-derives tools
|
||||
// for an already-created agent).
|
||||
// The caller normally passes the project structure, but a sub-agent can be
|
||||
// spawned without it (e.g. a re-derived turn). Fall back to the parent
|
||||
// request's stored project so `initialize_project` stays withheld while a
|
||||
// project is open — the same fallback the continue-turn path uses above.
|
||||
const parentRequest = localAiRequestsCache[parentAiRequestId];
|
||||
const hasOpenProject = Boolean(
|
||||
gameProjectJson || (parent && parent.gameProjectJson)
|
||||
gameProjectJson || (parentRequest && parentRequest.gameProjectJson)
|
||||
);
|
||||
const subAgentTools = withoutBackendOnlyTools(
|
||||
isStudioRoleId(roleId)
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"files": {
|
||||
"AI/CustomAIClient.js": {
|
||||
"bytes": 219321,
|
||||
"bytes": 219717,
|
||||
"origin": "newIDE/app/src/AI/CustomAIClient.js",
|
||||
"sha256": "8b0f606f756c29f82cfd5f15c0646c929abd072a80d4e77d18d4663f541b5bde"
|
||||
"sha256": "63dd86870cef349cdbaa0f6062ce4059fedeed6233e8ba4a65501f4078100188"
|
||||
},
|
||||
"AI/CustomAIClient.spec.js": {
|
||||
"bytes": 257794,
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
// Guard: the Tier-2 AI Studio runtime must not read the unqualified browser
|
||||
// globals that CRA's `no-restricted-globals` (eslint-config-react-app) bans.
|
||||
//
|
||||
// Why this exists: release run 38344881723 failed on all three OS jobs at
|
||||
// `npm run build` because `packages/ai-studio/AI/CustomAIClient.js` read the
|
||||
// global `parent` (it meant the parent request) — an eslint error the local
|
||||
// `npm test` suite could never see, since it never runs eslint. The failure
|
||||
// was invisible until a full release build.
|
||||
//
|
||||
// This is a TARGETED guard for that regression class, not a replacement for
|
||||
// the real lint. It scans only `parent`, `top`, `self` — the banned globals
|
||||
// that are implausible as deliberate local names — and it is a plain text
|
||||
// scan, so it can only prove absence of those three, not conformance to the
|
||||
// full rule. The release build remains the authority.
|
||||
//
|
||||
// The scan strips line comments, block comments and string/template literals
|
||||
// first, so the many prose/tool-description mentions of "parent" and "top"
|
||||
// (and `stepFrames` templates) do not trip it. Code inside a template
|
||||
// interpolation is treated as string text and skipped; no Tier-2 file relies
|
||||
// on that today.
|
||||
import { describe, it } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const here = path.dirname(fileURLToPath(import.meta.url));
|
||||
const PACKAGE = path.resolve(here, '..', '..', 'packages', 'ai-studio');
|
||||
|
||||
// The two directories the driver overlays onto newIDE/app/src (see
|
||||
// `TIER2_FILES` in scripts/apply-patches.mjs).
|
||||
const SCAN_DIRS = ['AI', path.join('AiGeneration', 'Studio')];
|
||||
const RESTRICTED = ['parent', 'top', 'self'];
|
||||
|
||||
// Blank out comments and string/template literals while preserving every
|
||||
// character OFFSET: a removed character becomes a space, a removed newline
|
||||
// stays a newline. That keeps reported line/column numbers identical to the
|
||||
// original source (matching what eslint would print).
|
||||
export function stripCommentsAndStrings(src) {
|
||||
let out = '';
|
||||
let i = 0;
|
||||
const n = src.length;
|
||||
const mask = ch => (ch === '\n' ? '\n' : ' ');
|
||||
while (i < n) {
|
||||
const c = src[i];
|
||||
const next = src[i + 1];
|
||||
if (c === '/' && next === '/') {
|
||||
while (i < n && src[i] !== '\n') {
|
||||
out += ' ';
|
||||
i++;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (c === '/' && next === '*') {
|
||||
out += ' ';
|
||||
i += 2;
|
||||
while (i < n && !(src[i] === '*' && src[i + 1] === '/')) {
|
||||
out += mask(src[i]);
|
||||
i++;
|
||||
}
|
||||
if (i < n) {
|
||||
out += ' ';
|
||||
i += 2;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (c === '"' || c === "'" || c === '`') {
|
||||
const quote = c;
|
||||
out += ' ';
|
||||
i++;
|
||||
while (i < n) {
|
||||
if (src[i] === '\\') {
|
||||
out += mask(src[i]);
|
||||
if (i + 1 < n) out += mask(src[i + 1]);
|
||||
i += 2;
|
||||
continue;
|
||||
}
|
||||
if (src[i] === quote) {
|
||||
out += ' ';
|
||||
i++;
|
||||
break;
|
||||
}
|
||||
out += mask(src[i]);
|
||||
i++;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
out += c;
|
||||
i++;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
export function findRestrictedGlobals(src) {
|
||||
const stripped = stripCommentsAndStrings(src);
|
||||
const hits = [];
|
||||
const lines = stripped.split('\n');
|
||||
for (let li = 0; li < lines.length; li++) {
|
||||
for (const name of RESTRICTED) {
|
||||
// Bare identifier: not preceded by an identifier char or a dot (so
|
||||
// `foo.parent` and `parentRequest` are fine) and not followed by one.
|
||||
const re = new RegExp(`(^|[^\\w$.])${name}\\b`, 'g');
|
||||
let m;
|
||||
while ((m = re.exec(lines[li])) !== null) {
|
||||
hits.push(`${name} (line ${li + 1}, col ${m.index + m[1].length + 1})`);
|
||||
}
|
||||
}
|
||||
}
|
||||
return hits;
|
||||
}
|
||||
|
||||
function collectTier2Sources() {
|
||||
const files = [];
|
||||
for (const dir of SCAN_DIRS) {
|
||||
const abs = path.join(PACKAGE, dir);
|
||||
for (const name of fs.readdirSync(abs)) {
|
||||
if (name.endsWith('.js') && !name.endsWith('.spec.js')) {
|
||||
files.push(path.join(abs, name));
|
||||
}
|
||||
}
|
||||
}
|
||||
return files;
|
||||
}
|
||||
|
||||
describe('unit: Tier-2 sources avoid banned browser globals', () => {
|
||||
it('no unqualified `parent`/`top`/`self` in the synced AI Studio runtime', () => {
|
||||
const files = collectTier2Sources();
|
||||
assert.ok(
|
||||
files.length >= 14,
|
||||
`expected the full Tier-2 surface (14 files), saw ${files.length}`
|
||||
);
|
||||
|
||||
const failures = [];
|
||||
for (const file of files) {
|
||||
const rel = path.relative(PACKAGE, file);
|
||||
for (const hit of findRestrictedGlobals(fs.readFileSync(file, 'utf8'))) {
|
||||
failures.push(`${rel}: ${hit}`);
|
||||
}
|
||||
}
|
||||
assert.equal(
|
||||
failures.length,
|
||||
0,
|
||||
`banned browser globals in Tier-2 sources:\n${failures.join('\n')}`
|
||||
);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user