Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .github/workflows/checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -140,12 +140,13 @@ jobs:
working-directory: web
run: bun install --frozen-lockfile

# 这三个是无框架的断言脚本,之前只在本地手动跑,CI 从来没执行过。
# 这些是无框架的断言脚本,之前只在本地手动跑,CI 从来没执行过。
# 放在构建之前:逻辑错误比打包错误更值得先失败。
- name: Test
run: |
bun web/src/utils/fileLibrary.test.cjs
bun web/src/utils/mermaidMindmap.test.cjs
bun web/src/utils/githubStatus.test.cjs
bun simple-mind-map/test/xmind-associative-line.test.cjs

- name: Build web app
Expand Down
3 changes: 2 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ cargo check --manifest-path desktop/src-tauri/Cargo.toml

bun web/src/utils/fileLibrary.test.cjs
bun web/src/utils/mermaidMindmap.test.cjs
bun web/src/utils/githubStatus.test.cjs
```

Use Node 20.11 or newer: the build toolchain relies on `import.meta.dirname`.
Expand All @@ -57,5 +58,5 @@ Updater artifacts are signed with the `TAURI_SIGNING_PRIVATE_KEY` secret; withou

## Pull requests

- One concern per pull request, linked to an issue. CI runs `title`, `format`, `cargo` and `build`; all four must pass before merge. `format` runs on Bun and owns Prettier, and `build` runs the three test scripts before bundling.
- One concern per pull request, linked to an issue. CI runs `title`, `format`, `cargo` and `build`; all four must pass before merge. `format` runs on Bun and owns Prettier, and `build` runs the test scripts before bundling.
- PR titles follow Conventional Commits — they become the squash commit subject.
1 change: 1 addition & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ Every issue carries one type label, usually one or two area labels, and `needs t
cargo fmt --manifest-path desktop/src-tauri/Cargo.toml --check
bun web/src/utils/fileLibrary.test.cjs
bun web/src/utils/mermaidMindmap.test.cjs
bun web/src/utils/githubStatus.test.cjs
bun simple-mind-map/test/xmind-associative-line.test.cjs
cd web && bun install --frozen-lockfile && npm run build
```
Expand Down
65 changes: 65 additions & 0 deletions desktop/src-tauri/src/github.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2043,6 +2043,9 @@ mod tests {
);
}
if first.starts_with("GET /user") {
if let Some((status, body)) = USER_RESPONSE.lock().unwrap().clone() {
return (status, body);
}
return (200, r#"{"login":"octocat"}"#.to_string());
}
if first.starts_with("GET /gists/missing") {
Expand Down Expand Up @@ -2123,6 +2126,22 @@ mod tests {
}

static AUTH_TEST: Mutex<()> = Mutex::new(());
static USER_RESPONSE: Mutex<Option<(u16, String)>> = Mutex::new(None);

struct UserResponseGuard;

impl UserResponseGuard {
fn set(status: u16, body: &str) -> Self {
*USER_RESPONSE.lock().unwrap() = Some((status, body.to_string()));
Self
}
}

impl Drop for UserResponseGuard {
fn drop(&mut self) {
*USER_RESPONSE.lock().unwrap() = None;
}
}

#[test]
fn device_flow_stores_token_without_returning_it() {
Expand Down Expand Up @@ -2155,6 +2174,10 @@ mod tests {
let polled = block(auth_poll(&dir.0)).expect("poll");
assert_eq!(polled.status, "success");
assert_eq!(polled.login, "octocat");
// 重新读状态,相当于侧栏卸载后再挂上:用户名必须还在。
let saved = status(&dir.0, "").expect("status");
assert!(saved.signed_in);
assert_eq!(saved.login, "octocat");
let rendered = serde_json::to_string(&polled).unwrap();
assert!(!rendered.contains("gho_test_token"));
assert!(!rendered.contains("device-123"));
Expand All @@ -2176,6 +2199,48 @@ mod tests {
assert_eq!(after.status, "cancelled");
}

#[test]
fn device_flow_stays_signed_in_when_login_lookup_fails() {
let _guard = AUTH_TEST.lock().unwrap();
let _user = UserResponseGuard::set(
403,
r#"{"message":"Resource not accessible by integration"}"#,
);
let dir = TempDir::new();
write_auth_unlocked(
&dir.0,
&AuthFile {
client_id: "Iv1.example".to_string(),
..AuthFile::default()
},
)
.unwrap();
let mock = Mock::start();
block(auth_start_with(
&dir.0,
&format!("{}/login/device/code", mock.base),
&format!("{}/login/oauth/access_token", mock.base),
&mock.base,
))
.expect("device code");
{
let mut gate = lock_gate().unwrap();
if let Some(pending) = gate.pending.as_mut() {
pending.last_poll = UNIX_EPOCH;
}
}
let polled = block(auth_poll(&dir.0)).expect("poll");
assert_eq!(polled.status, "success");
assert!(polled.login.is_empty());
let saved = status(&dir.0, "").expect("status");
assert!(saved.signed_in);
assert!(saved.login.is_empty());
let raw = fs::read_to_string(auth_path(&dir.0)).unwrap();
assert!(raw.contains("gho_test_token"));
assert!(!raw.contains("octocat"));
sign_out(&dir.0).unwrap();
}

#[test]
fn push_creates_secret_gist_and_pull_parses_it() {
let dir = TempDir::new();
Expand Down
10 changes: 5 additions & 5 deletions web/src/pages/Edit/components/DesktopHome.vue
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,7 @@
<script>
import { mapState } from 'vuex'
import { isMac, invoke } from '@/utils/desktop'
import { githubSidebarLabel } from '@/utils/githubStatus'

// 桌面端首页:直接以磁盘目录为结构,不维护额外索引
export default {
Expand Down Expand Up @@ -376,11 +377,10 @@ export default {
},

githubLabel() {
const status = this.githubStatus
if (status && status.signedIn && status.login) {
return this.$t('github.statusOn', { login: status.login })
}
return this.$t('github.statusOff')
const label = githubSidebarLabel(this.githubStatus)
return label.params
? this.$t(label.key, label.params)
: this.$t(label.key)
},

canMoveHere() {
Expand Down
15 changes: 15 additions & 0 deletions web/src/utils/githubStatus.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// 库侧栏的 GitHub 状态文案。和同步对话框同一套判断:
// 有令牌才算已登录;用户名只决定要不要带上 @login。
// 用户名缺失时仍显示已登录,不能落到「未登录」。
export function githubSidebarLabel(status) {
if (status && status.signedIn && status.login) {
return {
key: 'github.statusOn',
params: { login: status.login }
}
}
if (status && status.signedIn) {
return { key: 'github.signedInUnknown' }
}
return { key: 'github.statusOff' }
}
38 changes: 38 additions & 0 deletions web/src/utils/githubStatus.test.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
const assert = require('assert')
const esbuild = require('esbuild')
const path = require('path')
const vm = require('vm')

const bundled = esbuild.buildSync({
entryPoints: [path.join(__dirname, 'githubStatus.js')],
bundle: true,
format: 'cjs',
write: false,
platform: 'node'
})
const sandbox = { module: { exports: {} }, exports: {}, require }
sandbox.exports = sandbox.module.exports
vm.runInNewContext(bundled.outputFiles[0].text, sandbox)
const { githubSidebarLabel } = sandbox.module.exports

function assertLabel(status, key, login) {
const result = githubSidebarLabel(status)
assert.strictEqual(result.key, key)
if (login === undefined) {
assert.strictEqual(result.params, undefined)
return
}
assert.strictEqual(result.params.login, login)
}

assertLabel(null, 'github.statusOff')
assertLabel(undefined, 'github.statusOff')
assertLabel({}, 'github.statusOff')
assertLabel({ signedIn: false, login: 'octocat' }, 'github.statusOff')

assertLabel({ signedIn: true, login: 'octocat' }, 'github.statusOn', 'octocat')

// 有访问令牌但用户名还没写上时,侧栏不能显示「未登录」。
assertLabel({ signedIn: true, login: '' }, 'github.signedInUnknown')
assertLabel({ signedIn: true }, 'github.signedInUnknown')
assertLabel({ signedIn: true, login: null }, 'github.signedInUnknown')
Loading