Files
CyberStrikeAI/web/static/js/router-hash-form.test.cjs
T
Sycun cb8bc6b824 feat: update this installation's own source in one action
An operator running a fork has no honest way to upgrade: upgrade.sh downloads the
Release tarball of one hard-coded third-party repository and rsyncs it over the
tree, so "upgrade" means "overwrite yourself with somebody else's code", and the
safe alternative is to pull by hand and remember the build command.

internal/update drives the remote the directory already tracks: fetch, fast-forward,
go build into .update-staging, atomic rename of the binary (the previous one kept as
cyberstrike-ai.prev), and a .update-state.json so a rollback has a target that is
not guessed at. Refusals are the point - local source edits and a diverged branch
stop with the file names and commit counts in the error rather than being reset to
match, and operator-owned content (roles/skills/tools/agents/data/config.yaml) is put
aside before the merge and restored over it, with what was kept listed in keptContent
instead of quietly dropped.

Consumers: GET/POST /api/system/update* behind update:read / update:apply (mutations
are process-global, so they need a global-scope session); a console page at
#system-update with an apply button that names why it is blocked, a job polled from
the server and a rollback control; the same three actions as -check-update / -update
/ -update-rollback on the binary; OpenAPI entries for all five endpoints. Permission
gating no longer re-enables a control a page disabled for a reason of its own
(data-state-disabled), which every state-disabled, permission-gated button in the
console was subject to. upgrade.sh becomes a thin shell over the same implementation
and only falls back to the tarball path for installs that are not git work trees.

Docs and the zh/EN/RU dictionaries are updated. The package runs 13 tests against
real temporary git repositories; the HTTP layer, RBAC and the console page each
carry their own contract tests.
2026-10-07 12:07:26 +08:00

49 lines
2.4 KiB
JavaScript

// The console's own hash form is `#system-update`, not `#/system-update` - and before this,
// typing or sharing the slashed form silently did nothing: the id missed the whitelist and
// the router stayed on whatever page had loaded, with no message anywhere. Since every bit of
// documentation (and every human habit) writes the slashed form, the router now normalizes it
// instead of rejecting it.
const test = require('node:test');
const assert = require('node:assert');
const fs = require('node:fs');
const path = require('node:path');
const ROUTER = fs.readFileSync(path.join(__dirname, 'router.js'), 'utf8');
test('both hash entry points strip a leading slash before the whitelist', () => {
// Two parsers exist (initial load and hash change); fixing only one is how this class of
// bug survives a "works for me" check.
const sites = [...ROUTER.matchAll(/let pageId = hashParts\[0\]([^\n]*)/g)];
assert.ok(sites.length >= 2, `expected two hash parsers, found ${sites.length}`);
for (const s of sites) {
assert.match(s[1], /replace\(\/\^#\?\\\/\//, 'this parser does not normalize the slashed form: ' + s[0]);
}
});
test('the slashed form resolves to the same page id as the plain one', () => {
// Mirrors the router exactly: it takes location.hash.slice(1), so the '#' is already gone
// before the split. A helper here that expected the '#' would "pass" the wrong rule.
const normalize = (fullHash) => {
const raw = fullHash.startsWith('#') ? fullHash.slice(1) : fullHash;
return raw.split('?')[0].replace(/^#?\//, '');
};
for (const page of ['system-update', 'plugins-management', 'dashboard', 'chat']) {
assert.strictEqual(normalize('#' + page), page);
assert.strictEqual(normalize('#/' + page), page);
}
// Query-carrying hashes must keep working the same way they did.
assert.strictEqual(normalize('#chat?conversation=abc'), 'chat');
});
test('the docs use a form the router actually accepts', () => {
const docs = ['../../../docs/zh-CN/developer-guide.md', '../../../docs/en-US/developer-guide.md'];
for (const rel of docs) {
const p = path.join(__dirname, rel);
const text = fs.readFileSync(p, 'utf8');
// Not a style preference: `#/x` was a dead link when it was written, so a doc that
// tells an operator to open it is telling them to open nothing.
assert.ok(!/#\/system-update/.test(text), `${rel} still advertises the dead #/system-update form`);
assert.match(text, /#system-update/, `${rel} must name the form that works`);
}
});