Skip to content

Commit 6e9072b

Browse files
committed
fix: the update feed, the update check and the init switch are user settings only
levelcode-ai's settings became application-scoped in #113, but the other bundled extensions' settings were left, and a folder's .vscode/settings.json can still set them, trusted or in Restricted Mode (untrustedWorkspaces is true for these extensions, so VS Code applies no restriction): - levelcode.update.url overrides the update feed, so a folder can make the editor's update check offer a download from the folder's own address. - levelcode.update.enabled and .intervalHours switch that check off or change how often it asks. - levelcode.init.enabled decides whether the user's ~/.levelcode/init.js runs at start. All four are now "scope": "application". The npp-pack's six editing preferences stay folder-settable, written out as an explicit list. New suite levelcode-updater/test/bundledSettingsScope.test.js is default-deny over every bundled extension except levelcode-ai, like levelcode-ai's own settingsScope test: a setting added later is folder-settable only if someone puts it on the list. It fails on the v1.4.1 manifests naming exactly these four settings. Mutations caught: update.url unscoped, a misspelt scope, a new unscoped setting, init.enabled window-scoped.
1 parent ecda386 commit 6e9072b

3 files changed

Lines changed: 106 additions & 0 deletions

File tree

‎extensions/levelcode-hackability/package.json‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@
5858
"properties": {
5959
"levelcode.init.enabled": {
6060
"type": "boolean",
61+
"scope": "application",
6162
"default": true,
6263
"description": "Run your user init script (~/.levelcode/init.js) when LevelCode starts."
6364
}

‎extensions/levelcode-updater/package.json‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,16 +34,19 @@
3434
"properties": {
3535
"levelcode.update.enabled": {
3636
"type": "boolean",
37+
"scope": "application",
3738
"default": true,
3839
"description": "Periodically check for new LevelCode releases and notify with a download link (does not auto-install)."
3940
},
4041
"levelcode.update.url": {
4142
"type": "string",
43+
"scope": "application",
4244
"default": "",
4345
"description": "Update feed base URL. Overrides product.json updateUrl. Leave blank to use product.json. For local testing, point at the reference update server (e.g. http://localhost:9696)."
4446
},
4547
"levelcode.update.intervalHours": {
4648
"type": "number",
49+
"scope": "application",
4750
"default": 4,
4851
"minimum": 1,
4952
"description": "How often to check for updates, in hours."
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Which settings of the OTHER bundled extensions a folder can set — run: node test/bundledSettingsScope.test.js
3+
*
4+
* levelcode-ai keeps its own list (extensions/levelcode-ai/test/settingsScope.test.js). This covers
5+
* every other extension LevelCode ships: levelcode-updater, levelcode-hackability (the init script)
6+
* and levelcode-npp-pack. The same mechanism applies. A setting with no "scope" is read from the open
7+
* folder's .vscode/settings.json too, and the folder's value wins over the user's; Workspace Trust
8+
* does not change that for an extension that says untrustedWorkspaces.supported = true.
9+
*
10+
* Why it matters here: `levelcode.update.url` overrides the update feed, so a folder that sets it makes
11+
* the editor's update check offer a download from the folder's own address. `levelcode.update.enabled`
12+
* and `.intervalHours` switch that check off or change how often it asks, and `levelcode.init.enabled`
13+
* decides whether the user's ~/.levelcode/init.js runs at start.
14+
*
15+
* Default-deny, as in levelcode-ai: every setting is application-scoped except an explicit list of
16+
* editing preferences below. A setting added later is folder-settable only if someone puts it on that
17+
* list, and the first test here fails until they do.
18+
*--------------------------------------------------------------------------------------------*/
19+
// @ts-check
20+
'use strict';
21+
22+
const assert = require('assert');
23+
const fs = require('fs');
24+
const path = require('path');
25+
26+
const extensionsDir = path.join(__dirname, '..', '..');
27+
28+
/** Every contributed setting of every bundled extension except levelcode-ai, with the extension it came from. */
29+
function bundledSettings(dir = extensionsDir) {
30+
/** @type {Record<string, { ext: string, scope: string | undefined }>} */
31+
const out = {};
32+
for (const ext of fs.readdirSync(dir).sort()) {
33+
if (ext === 'levelcode-ai') { continue; }
34+
const file = path.join(dir, ext, 'package.json');
35+
if (!fs.existsSync(file)) { continue; }
36+
const pkg = JSON.parse(fs.readFileSync(file, 'utf8'));
37+
const conf = pkg.contributes && pkg.contributes.configuration;
38+
if (!conf) { continue; }
39+
for (const section of Array.isArray(conf) ? conf : [conf]) {
40+
for (const [key, def] of Object.entries(section.properties || {})) { out[key] = { ext, scope: def.scope }; }
41+
}
42+
}
43+
return out;
44+
}
45+
const props = bundledSettings();
46+
47+
let n = 0;
48+
function test(name, fn) { fn(); n++; console.log(' ok - ' + name); }
49+
50+
// Scopes a folder's .vscode/settings.json cannot set. 'machine' is user (or remote) settings only,
51+
// like 'application', but kept per profile.
52+
const USER_ONLY = new Set(['application', 'machine']);
53+
const KNOWN_SCOPES = new Set(['application', 'machine', 'window', 'resource', 'language-overridable', 'machine-overridable']);
54+
55+
// Written out here on purpose: opening a setting to folders is a decision, and this is where it is made.
56+
// These shape how the editor treats text and big files. None of them decides where anything is sent,
57+
// what runs, or what is trusted.
58+
const FOLDER_OK = [
59+
'levelcode.bigFile.notify', 'levelcode.bigFile.thresholdMB',
60+
'levelcode.jsonPaste.enabled', 'levelcode.jsonPaste.indent',
61+
'levelcode.macros.maxReplayCount', 'levelcode.macros.maxSteps',
62+
];
63+
64+
test('a folder can set only the listed editing preferences', () => {
65+
const open = Object.keys(props).filter((k) => !USER_ONLY.has(String(props[k].scope))).sort();
66+
assert.deepStrictEqual(open, FOLDER_OK, 'a setting a folder can set that is not on the list: ' + open.filter((k) => !FOLDER_OK.includes(k)).join(', '));
67+
for (const k of FOLDER_OK) { assert.ok(k in props, k + ' is on the list but is not a setting: stale entry'); }
68+
});
69+
70+
test('every scope is one the editor knows (it reads anything else as "window", which a folder can set)', () => {
71+
for (const [k, def] of Object.entries(props)) {
72+
if (def.scope !== undefined) { assert.ok(KNOWN_SCOPES.has(def.scope), k + ' has scope ' + JSON.stringify(def.scope)); }
73+
}
74+
});
75+
76+
test('the update feed and the update check are user settings only', () => {
77+
for (const k of ['levelcode.update.url', 'levelcode.update.enabled', 'levelcode.update.intervalHours']) {
78+
assert.ok(k in props, k + ' is not a setting');
79+
assert.strictEqual(props[k].ext, 'levelcode-updater', k);
80+
assert.strictEqual(props[k].scope, 'application', k + ' can be set by a folder');
81+
}
82+
});
83+
84+
test('whether the user init script runs is a user setting only', () => {
85+
assert.ok('levelcode.init.enabled' in props, 'levelcode.init.enabled is not a setting');
86+
assert.strictEqual(props['levelcode.init.enabled'].scope, 'application', 'levelcode.init.enabled can be set by a folder');
87+
});
88+
89+
test('the check sees a setting added to any bundled extension (default-deny is not just for the ones known today)', () => {
90+
// A throwaway extensions tree with one new, unscoped setting: the collector must report it as folder-settable.
91+
const tmp = fs.mkdtempSync(path.join(require('os').tmpdir(), 'lc-scope-'));
92+
try {
93+
fs.mkdirSync(path.join(tmp, 'levelcode-newthing'));
94+
fs.writeFileSync(path.join(tmp, 'levelcode-newthing', 'package.json'),
95+
JSON.stringify({ contributes: { configuration: { properties: { 'levelcode.newthing.url': { type: 'string' } } } } }));
96+
const found = bundledSettings(tmp);
97+
assert.deepStrictEqual(Object.keys(found), ['levelcode.newthing.url']);
98+
assert.ok(!USER_ONLY.has(String(found['levelcode.newthing.url'].scope)), 'an unscoped setting must count as folder-settable');
99+
} finally { fs.rmSync(tmp, { recursive: true, force: true }); }
100+
});
101+
102+
console.log(`\n${n} tests passed`);

0 commit comments

Comments
 (0)