browser-assistant 2.8.22 → 2.8.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/extension/background.js +2 -2
- package/extension/content.js +2 -0
- package/extension/editor-bridge.js +53 -27
- package/extension/manifest.json +1 -1
- package/package.json +1 -1
- package/server/main.py +1 -1
package/extension/background.js
CHANGED
|
@@ -286,10 +286,10 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
|
|
|
286
286
|
return { ok: false, error: 'The portal submit command and IDE workspace do not match.' };
|
|
287
287
|
if (request.action === 'runOnlineIde' && (!ide.active || typeof request.code !== 'string'
|
|
288
288
|
|| !request.code.trim() || new TextEncoder().encode(request.code).length > 12000)) {
|
|
289
|
-
return { ok: false, error: 'The matching IDE must be active and the source must
|
|
289
|
+
return { ok: false, error: 'The matching IDE must be active and the app.js source must be at most 12 KB.' };
|
|
290
290
|
}
|
|
291
291
|
const results = await chrome.scripting.executeScript({ target: { tabId: ide.id, allFrames: true }, world: 'MAIN',
|
|
292
|
-
func:
|
|
292
|
+
func: browserAssistantOnlineIde,
|
|
293
293
|
args: [request.action, workspace, questionId, request.token, request.action === 'runOnlineIde' ? request.code : null] });
|
|
294
294
|
const frameResults = results.map(entry => entry.result).filter(Boolean);
|
|
295
295
|
return frameResults.length === 1 ? frameResults[0] : { ok: false, retry: true, error: 'The IDE workspace is still opening.' };
|
package/extension/content.js
CHANGED
|
@@ -1211,6 +1211,8 @@ function mountJournalCountdown(signal) {
|
|
|
1211
1211
|
if (result?.done) {
|
|
1212
1212
|
if (result.passed)
|
|
1213
1213
|
automation.completed = 1;
|
|
1214
|
+
else if (result.saveUnconfirmed)
|
|
1215
|
+
automation.error = 'Code was pasted into app.js, but the saved file did not match. Check app.js and save before running it.';
|
|
1214
1216
|
else
|
|
1215
1217
|
automation.error = result.submitted ? 'Grading finished. Review the test results in the IDE.' : 'The IDE command finished without a confirmed submission. Check the terminal.';
|
|
1216
1218
|
stopAutomation(result.passed ? 'COMPLETED' : 'CHECK_IDE_RESULT');
|
|
@@ -88,9 +88,9 @@ function browserAssistantEditor(operation, selector, expected, value) {
|
|
|
88
88
|
return { ok: false, error: error.message };
|
|
89
89
|
}
|
|
90
90
|
}
|
|
91
|
-
//
|
|
92
|
-
//
|
|
93
|
-
async function
|
|
91
|
+
// Run the IDE's normal paste and autosave flow. The terminal only verifies and
|
|
92
|
+
// grades the saved file; generated source is never passed to the shell.
|
|
93
|
+
async function browserAssistantOnlineIde(action, workspace, questionId, token, source) {
|
|
94
94
|
if (location.origin !== 'https://ide.ccbp.in' || location.pathname.replace(/\/$/, '') !== `/playground/${workspace}`)
|
|
95
95
|
return null;
|
|
96
96
|
const visible = node => node.getClientRects().length > 0;
|
|
@@ -101,6 +101,7 @@ async function browserAssistantOnlineIdeTerminal(action, workspace, questionId,
|
|
|
101
101
|
const rowContainer = input.closest('.xterm')?.querySelector('.xterm-rows');
|
|
102
102
|
const rows = () => [...(rowContainer?.children || [])].map(row => row.textContent.trim()).filter(Boolean);
|
|
103
103
|
const finishMarker = `BA_RESULT_${token}`;
|
|
104
|
+
const saveMarker = `BA_APP_JS_NOT_SAVED_${token}`;
|
|
104
105
|
const reportPrefix = `https://learning.ccbp.in/ide-coding-submission/${questionId}/`;
|
|
105
106
|
const reports = lines => {
|
|
106
107
|
const text = lines.join('');
|
|
@@ -119,7 +120,7 @@ async function browserAssistantOnlineIdeTerminal(action, workspace, questionId,
|
|
|
119
120
|
const report = index >= 0 && reports(lines.slice(index)).find(id => !previous.reports.includes(id));
|
|
120
121
|
const passed = result === `${finishMarker}:0` && Boolean(report) && index > 0
|
|
121
122
|
&& lines.slice(Math.max(0, index - 3), index).includes('All test cases passed');
|
|
122
|
-
return { ok: true, done: true, passed, submitted: Boolean(report), url: report ? reportPrefix + report : null };
|
|
123
|
+
return { ok: true, done: true, passed, submitted: Boolean(report), saveUnconfirmed: lines.includes(saveMarker), url: report ? reportPrefix + report : null };
|
|
123
124
|
}
|
|
124
125
|
if (previous?.token === token)
|
|
125
126
|
return { ok: false, error: 'This IDE run was already started. Check the terminal.' };
|
|
@@ -146,35 +147,60 @@ async function browserAssistantOnlineIdeTerminal(action, workspace, questionId,
|
|
|
146
147
|
return { ok: false, retry: true, error: 'Waiting for the workspace app.js file in Explorer.' };
|
|
147
148
|
files[0].click();
|
|
148
149
|
await new Promise(resolve => setTimeout(resolve, 200));
|
|
149
|
-
const
|
|
150
|
-
|
|
151
|
-
|
|
150
|
+
const findEditor = () => {
|
|
151
|
+
const editors = [...document.querySelectorAll('.monaco-editor[data-uri]')].filter(visible);
|
|
152
|
+
const node = editors.length === 1 && editors[0];
|
|
153
|
+
const uri = node && node.getAttribute('data-uri');
|
|
154
|
+
return uri && uri.startsWith('file:') && uri.endsWith(`/${prompt[2]}/app.js`) ? node : null;
|
|
155
|
+
};
|
|
156
|
+
const editor = findEditor();
|
|
157
|
+
if (!editor) {
|
|
152
158
|
return { ok: false, retry: true, error: 'Waiting for the app.js editor.' };
|
|
153
159
|
}
|
|
160
|
+
const editorInput = editor.querySelector('textarea.inputarea');
|
|
154
161
|
const renderedCode = editor.querySelector('.view-lines');
|
|
155
|
-
if (!
|
|
156
|
-
return { ok: false, error: '
|
|
157
|
-
const
|
|
158
|
-
const
|
|
162
|
+
if (!editorInput || !renderedCode?.querySelector('.view-line'))
|
|
163
|
+
return { ok: false, retry: true, error: 'Waiting for the app.js editor input.' };
|
|
164
|
+
const model = window.monaco?.editor?.getEditors?.().find(instance => instance.getDomNode() === editor)?.getModel();
|
|
165
|
+
const isEmpty = () => {
|
|
166
|
+
if (model)
|
|
167
|
+
return model.getValue() === '';
|
|
168
|
+
// The portal bundles Monaco without a global editor API. Require the
|
|
169
|
+
// visible first and only row to be blank; never select or replace text.
|
|
170
|
+
const lineNumbers = editor.querySelectorAll('.margin-view-overlays .line-numbers');
|
|
171
|
+
return lineNumbers.length === 1 && lineNumbers[0].textContent.trim() === '1'
|
|
172
|
+
&& renderedCode.querySelectorAll('.view-line').length === 1 && !renderedCode.textContent.trim();
|
|
173
|
+
};
|
|
174
|
+
if (!isEmpty())
|
|
175
|
+
return { ok: false, error: 'app.js is not empty or its contents could not be checked. Review the code in the editor.' };
|
|
176
|
+
const code = source.replace(/\r\n?/g, '\n');
|
|
177
|
+
const bytes = new TextEncoder().encode(code);
|
|
159
178
|
const digest = Array.from(new Uint8Array(await crypto.subtle.digest('SHA-256', bytes)), byte => byte.toString(16).padStart(2, '0')).join('');
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
'
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
179
|
+
if (document.visibilityState !== 'visible' || findEditor() !== editor || !isEmpty()) {
|
|
180
|
+
return { ok: false, error: 'The app.js editor changed before pasting.' };
|
|
181
|
+
}
|
|
182
|
+
editorInput.focus();
|
|
183
|
+
if (document.activeElement !== editorInput || editorInput.readOnly || editorInput.disabled) {
|
|
184
|
+
return { ok: false, error: 'The app.js editor is not available for editing.' };
|
|
185
|
+
}
|
|
186
|
+
const pasteData = new DataTransfer();
|
|
187
|
+
pasteData.setData('text/plain', code);
|
|
188
|
+
editorInput.dispatchEvent(new ClipboardEvent('paste', { clipboardData: pasteData, bubbles: true, cancelable: true, composed: true }));
|
|
189
|
+
const pasteDeadline = Date.now() + 1500;
|
|
190
|
+
const didPaste = () => model ? model.getValue() === code : Boolean(renderedCode.textContent.trim());
|
|
191
|
+
while (!didPaste() && Date.now() < pasteDeadline && document.activeElement === editorInput && findEditor() === editor) {
|
|
192
|
+
await new Promise(resolve => setTimeout(resolve, 50));
|
|
193
|
+
}
|
|
194
|
+
if (!didPaste() || findEditor() !== editor || document.activeElement !== editorInput || document.visibilityState !== 'visible') {
|
|
195
|
+
return { ok: false, error: 'The IDE did not confirm the paste into app.js. Check the editor before trying again.' };
|
|
196
|
+
}
|
|
197
|
+
// Check exact saved bytes before grading; allow time for the IDE's autosave.
|
|
198
|
+
// All interpolated values have restricted alphabets.
|
|
199
|
+
const saved = `printf '%s app.js\\n' '${digest}' | sha256sum --check --status`;
|
|
174
200
|
const echoMarker = `BA_COMMAND_${token}`;
|
|
175
|
-
const command = `
|
|
201
|
+
const command = `( for ba_try in 1 2 3 4 5 6 7 8 9 10; do ${saved} && break; sleep 0.5; done; if ${saved}; then node --check app.js && ccbp submit ${workspace}; else printf '%s\\n' ${saveMarker}; false; fi ); printf '\\n${finishMarker}:%s\\n' "$?" #${echoMarker}`;
|
|
176
202
|
if (rows().at(-1) !== before.at(-1))
|
|
177
|
-
return { ok: false, error: '
|
|
203
|
+
return { ok: false, error: 'Code is in app.js, but the IDE terminal changed before grading.' };
|
|
178
204
|
input.focus();
|
|
179
205
|
if (document.activeElement !== input)
|
|
180
206
|
return { ok: false, error: 'The IDE terminal could not be focused.' };
|
package/extension/manifest.json
CHANGED
package/package.json
CHANGED
package/server/main.py
CHANGED
|
@@ -191,7 +191,7 @@ async def lifespan(_app: FastAPI):
|
|
|
191
191
|
client.close()
|
|
192
192
|
|
|
193
193
|
|
|
194
|
-
app = FastAPI(title="Browser Assistant API", version="2.8.
|
|
194
|
+
app = FastAPI(title="Browser Assistant API", version="2.8.23", lifespan=lifespan)
|
|
195
195
|
install_local_guards(app)
|
|
196
196
|
|
|
197
197
|
@app.exception_handler(HTTPException)
|