mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-05 00:47:48 +08:00
195 lines
11 KiB
JavaScript
195 lines
11 KiB
JavaScript
"use strict";
|
|
|
|
const assert = require("node:assert/strict");
|
|
const fs = require("node:fs");
|
|
const path = require("node:path");
|
|
const test = require("node:test");
|
|
|
|
const { createAetherEmbedBridge, isAetherEmbed } = require("../public/embed-bridge.js");
|
|
const i18n = require("../public/i18n.js");
|
|
|
|
function embeddedWindow() {
|
|
const listeners = new Map();
|
|
const posts = [];
|
|
const parent = { postMessage: (message, origin) => posts.push({ message, origin }) };
|
|
const bodyClasses = new Set();
|
|
const documentElement = { dataset: {}, style: {} };
|
|
const windowLike = {
|
|
location: { search: "?embed=aether", origin: "https://aether.example" },
|
|
parent,
|
|
document: {
|
|
body: { classList: { add: (value) => bodyClasses.add(value) } },
|
|
documentElement,
|
|
},
|
|
addEventListener: (name, listener) => listeners.set(name, listener),
|
|
removeEventListener: (name, listener) => {
|
|
if (listeners.get(name) === listener) listeners.delete(name);
|
|
},
|
|
};
|
|
return { bodyClasses, documentElement, listeners, parent, posts, windowLike };
|
|
}
|
|
|
|
test("Aether embed mode is opt-in and announces readiness only to the same-origin parent", () => {
|
|
assert.equal(isAetherEmbed({ search: "" }), false);
|
|
assert.equal(isAetherEmbed({ search: "?embed=other" }), false);
|
|
assert.equal(isAetherEmbed({ search: "?embed=aether" }), true);
|
|
|
|
const fixture = embeddedWindow();
|
|
const bridge = createAetherEmbedBridge(fixture.windowLike);
|
|
assert.equal(bridge.active, true);
|
|
bridge.start();
|
|
assert.equal(fixture.bodyClasses.has("embed-aether"), true);
|
|
assert.deepEqual(fixture.posts, [{
|
|
message: { v: 1, type: "aether-vscodex/ready" },
|
|
origin: "https://aether.example",
|
|
}]);
|
|
});
|
|
|
|
test("Aether embed bridge rejects cross-origin and non-parent messages and buffers an early connect", () => {
|
|
const fixture = embeddedWindow();
|
|
const bridge = createAetherEmbedBridge(fixture.windowLike);
|
|
bridge.start();
|
|
const dispatch = fixture.listeners.get("message");
|
|
const connect = {
|
|
v: 1,
|
|
type: "aether-vscodex/connect",
|
|
ticket: "one-time-ticket",
|
|
wsUrl: "/api/vscodex/ws",
|
|
locale: "en-US",
|
|
theme: "dark",
|
|
};
|
|
|
|
dispatch({ origin: "https://attacker.example", source: fixture.parent, data: connect });
|
|
dispatch({ origin: "https://aether.example", source: {}, data: connect });
|
|
let received = null;
|
|
bridge.on("connect", (message) => { received = message; });
|
|
assert.equal(received, null);
|
|
|
|
dispatch({ origin: "https://aether.example", source: fixture.parent, data: connect });
|
|
assert.equal(received.ticket, "one-time-ticket");
|
|
assert.equal(fixture.documentElement.dataset.theme, "dark");
|
|
|
|
const second = embeddedWindow();
|
|
const bufferedBridge = createAetherEmbedBridge(second.windowLike);
|
|
bufferedBridge.start();
|
|
second.listeners.get("message")({ origin: "https://aether.example", source: second.parent, data: connect });
|
|
let buffered = null;
|
|
bufferedBridge.on("connect", (message) => { buffered = message; });
|
|
assert.equal(buffered.ticket, "one-time-ticket");
|
|
});
|
|
|
|
test("bridge ticket requests never place the ticket in a URL", () => {
|
|
const fixture = embeddedWindow();
|
|
const bridge = createAetherEmbedBridge(fixture.windowLike);
|
|
bridge.start();
|
|
bridge.requestTicket({ reason: "disconnected", deviceId: "device-1" });
|
|
assert.deepEqual(fixture.posts.at(-1), {
|
|
message: {
|
|
v: 1,
|
|
type: "aether-vscodex/request-ticket",
|
|
reason: "disconnected",
|
|
deviceId: "device-1",
|
|
},
|
|
origin: "https://aether.example",
|
|
});
|
|
});
|
|
|
|
test("locale dictionary covers static shell and core dynamic status text", () => {
|
|
assert.equal(i18n.translate("设置", "en-US"), "Settings");
|
|
assert.equal(i18n.translate("中文", "en-US"), "Chinese");
|
|
assert.equal(i18n.translate("正在思考", "en-US"), "Thinking");
|
|
assert.equal(i18n.translate("已读取这些内容 · 4 个文件", "en-US"), "Read these items · 4 files");
|
|
assert.equal(i18n.translate("用时 3分45秒", "en-US"), "Worked for 3m45s");
|
|
assert.equal(i18n.translate("修改权限,当前为需要时询问", "en-US"), "Change permissions. Current: Ask when needed");
|
|
assert.equal(i18n.translate("模型设置更新失败:timeout", "en-US"), "Unable to update model settings: timeout");
|
|
assert.equal(i18n.translate("请求 #17 已发送,等待 VS Code 主机确认", "en-US"), "Request #17 sent; waiting for the VS Code host");
|
|
assert.equal(i18n.translate("无法读取 notes.md", "en-US"), "Unable to read notes.md");
|
|
assert.equal(i18n.translate("命令: timed out", "en-US"), "Command: timed out");
|
|
assert.equal(i18n.translate("命令: timed out(执行状态未知,请等待主机恢复)", "en-US"), "Command: timed out (execution status unknown; wait for the host to recover)");
|
|
assert.equal(i18n.translate("子代理 失败", "en-US"), "Subagent failed");
|
|
assert.equal(i18n.translate("已在 2秒 内运行 echo hi", "en-US"), "Ran echo hi in 2s");
|
|
assert.equal(i18n.translate("命令运行失败 · echo hi · 2秒", "en-US"), "Command failed · echo hi · 2s");
|
|
assert.equal(i18n.translate("命令运行失败 · echo hi", "en-US"), "Command failed · echo hi");
|
|
assert.equal(i18n.translate("已停止 echo hi · 2秒", "en-US"), "Stopped echo hi · 2s");
|
|
assert.equal(i18n.translate("文件变更 · 失败", "en-US"), "File changes · Failed");
|
|
assert.equal(i18n.translate("文件变更 · 已中断", "en-US"), "File changes · Interrupted");
|
|
assert.equal(i18n.translate("命令 · echo hi", "en-US"), "Command · echo hi");
|
|
assert.equal(i18n.translate("命令 · 设置", "en-US"), "Command · 设置");
|
|
assert.equal(i18n.translate("正在读取 设置", "en-US"), "Reading 设置");
|
|
assert.equal(i18n.translate("已在 2秒 内运行 设置", "en-US"), "Ran 设置 in 2s");
|
|
assert.equal(i18n.translate("正在切换到「设置」…", "en-US"), "Switching to “设置”...");
|
|
assert.equal(i18n.translate("你停止了工作", "en-US"), "You stopped working");
|
|
assert.equal(i18n.translate("工具失败", "en-US"), "Tool failed");
|
|
assert.equal(i18n.translate("正在搜索", "en-US"), "Searching");
|
|
assert.equal(i18n.translate("已工具 · 2秒", "en-US"), "Tool completed · 2s");
|
|
assert.equal(i18n.translate("当前模型 5.6 Sol 标准,切换模型", "en-US"), "Current model: 5.6 Sol Medium. Change model");
|
|
assert.equal(i18n.translate("编辑了文件", "en-US"), "Edited files");
|
|
assert.equal(i18n.translate("编辑了文件 · 2秒", "en-US"), "Edited files · 2s");
|
|
assert.equal(i18n.translate("已完成计划", "en-US"), "Completed plan");
|
|
assert.equal(i18n.translate("已完成计划 · 2秒", "en-US"), "Completed plan · 2s");
|
|
assert.equal(i18n.translate("…(文件已截断)", "en-US"), "... (file truncated)");
|
|
assert.equal(i18n.translate("事件窗口已过期,请以当前快照为准", "en-US"), "The event window expired; the current snapshot is authoritative");
|
|
assert.equal(i18n.translate("控制模式", "en-US"), "Control mode");
|
|
assert.equal(i18n.translate("同步模式跟随 VS Code 当前会话", "en-US"), "Sync mode follows the current VS Code conversation");
|
|
assert.equal(i18n.translate("异步模式可独立管理会话", "en-US"), "Async mode manages conversations independently");
|
|
assert.equal(i18n.translate("当前任务或请求完成后才能切换控制模式", "en-US"), "The control mode can be changed after the current task or request finishes");
|
|
assert.equal(i18n.translate("Settings", "zh-CN"), "设置");
|
|
assert.equal(i18n.normalizeLocale("zh-Hans"), "zh-CN");
|
|
assert.equal(i18n.normalizeLocale("en-GB"), "en-US");
|
|
});
|
|
|
|
test("renderer-owned dynamic labels have English fallbacks without translating host values", () => {
|
|
assert.equal(i18n.translate("命令 · echo hi", "en-US"), "Command · echo hi");
|
|
assert.equal(i18n.translate("你停止了工作", "en-US"), "You stopped working");
|
|
assert.equal(i18n.translate("工具失败", "en-US"), "Tool failed");
|
|
assert.equal(i18n.translate("正在搜索", "en-US"), "Searching");
|
|
assert.equal(i18n.translate("当前模型 5.6 Sol 标准,切换模型", "en-US"), "Current model: 5.6 Sol Medium. Change model");
|
|
|
|
const app = fs.readFileSync(path.join(__dirname, "..", "public", "app.js"), "utf8");
|
|
// Command/path/title values are appended after a locale-specific prefix;
|
|
// they are never passed through the translator as a whole.
|
|
assert.match(app, /uiWithRaw\("正在运行 ", "Running ",/);
|
|
assert.match(app, /uiWithRaw\("已读取 ", "Read ",/);
|
|
assert.match(app, /uiLocale\(\) === "en-US" \? `Switching to/);
|
|
});
|
|
|
|
test("public shell uses relative assets and embedded startup skips the health probe", () => {
|
|
const publicRoot = path.join(__dirname, "..", "public");
|
|
const html = fs.readFileSync(path.join(publicRoot, "index.html"), "utf8");
|
|
const app = fs.readFileSync(path.join(publicRoot, "app.js"), "utf8");
|
|
assert.match(html, /href="\.\/style\.css"/);
|
|
assert.match(html, /src="\.\/embed-bridge\.js"/);
|
|
assert.match(html, /src="\.\/i18n\.js"/);
|
|
assert.match(html, /src="\.\/app\.js"/);
|
|
assert.match(app, /if \(embeddedInAether\)[\s\S]+else \{[\s\S]+fetch\("\.\/api\/health"/);
|
|
assert.doesNotMatch(app, /ticket=.*state\.embedTicket/);
|
|
assert.match(app, /empty\.textContent = t\(activity\.status === "inProgress" \? "正在读取文件" : "读取完成"\)/);
|
|
assert.match(app, /outputContent\.textContent = t\("无输出"\)/);
|
|
assert.match(app, /button\.title = t\(title\)/);
|
|
assert.match(app, /activity\.action === "spawnAgent" \? t\("启动子代理"\)/);
|
|
assert.match(app, /return t\("需要远程确认或输入"\)/);
|
|
assert.match(app, /questionPrompt === undefined \|\| questionPrompt === null \? t\("请输入"\)/);
|
|
assert.match(app, /checkbox\.setAttribute\("aria-label", t\(checkbox\.checked \? "已完成" : "未完成"\)\)/);
|
|
assert.match(app, /window\.addEventListener\("aether-vscodex:locale", \(\) => \{[\s\S]+state\.activities\.values\(\)[\s\S]+renderRequests\(\)/);
|
|
});
|
|
|
|
test("control mode is snapshot-authoritative and gates independent session actions", () => {
|
|
const publicRoot = path.join(__dirname, "..", "public");
|
|
const html = fs.readFileSync(path.join(publicRoot, "index.html"), "utf8");
|
|
const app = fs.readFileSync(path.join(publicRoot, "app.js"), "utf8");
|
|
|
|
assert.match(html, /id="controlModeSwitch"[\s\S]+data-control-mode="sync"[\s\S]+data-control-mode="async"/);
|
|
assert.match(app, /command\("control\/mode\/set", \{ mode \}\)/);
|
|
assert.match(app, /applyControlModeSnapshot\(payload\.metadata\)/);
|
|
assert.match(app, /const controlMetadata = \{[\s\S]+snapshot\.metadata[\s\S]+appState\.sessionMetadata[\s\S]+applyControlModeSnapshot\(controlMetadata\)/);
|
|
assert.match(app, /sessionList: source\.sessionList === true/);
|
|
assert.match(app, /Boolean\(state\.sessionListCommandId\)/);
|
|
assert.match(app, /mode_switch_pending.*return "正在切换控制模式"/);
|
|
assert.match(app, /mode_busy\|cannot switch control mode.*return "当前任务或请求完成后才能切换控制模式"/);
|
|
assert.match(app, /setConversationStatus\(sessionErrorMessage\(message, "控制模式切换失败"\), "warning"\)/);
|
|
assert.match(app, /if \(!sessionControlAllowed\("sessionList"\)\) return;/);
|
|
assert.match(app, /if \(!sessionControlAllowed\("sessionSelect"\)\)/);
|
|
assert.match(app, /if \(!sessionControlAllowed\("sessionCreate"\)\)/);
|
|
assert.match(app, /sessionPickerButton\.disabled = !listAllowed/);
|
|
});
|