Skip to content

Commit 903c04b

Browse files
authored
Merge pull request #9 from JasonHonKL/dev/roadmap
update the repl mode
2 parents e8c3c05 + a5eebfb commit 903c04b

147 files changed

Lines changed: 21077 additions & 843 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.cargo/config.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
[env]
2+
LIBCLANG_PATH = "/opt/homebrew/opt/llvm/lib"

.github/workflows/ci.yml

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches: [main, master]
6+
pull_request:
7+
branches: [main, master]
8+
9+
jobs:
10+
test:
11+
name: test (${{ matrix.os }})
12+
runs-on: ${{ matrix.os }}
13+
strategy:
14+
fail-fast: false
15+
matrix:
16+
os: [ubuntu-latest, macos-latest]
17+
steps:
18+
- uses: actions/checkout@v4
19+
20+
- name: Install Rust (nightly)
21+
uses: dtolnay/rust-toolchain@nightly
22+
23+
- name: Cache cargo
24+
uses: Swatinem/rust-cache@v2
25+
with:
26+
key: ${{ matrix.os }}
27+
28+
- name: Install system dependencies (linux)
29+
if: runner.os == 'Linux'
30+
run: |
31+
sudo apt-get update
32+
sudo apt-get install -y --no-install-recommends libssl-dev pkg-config
33+
34+
- name: Run tests
35+
run: cargo test --all-features

.gitignore

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
**/*.rs.bk
33
Cargo.lock
44
.DS_Store
5+
**/.DS_Store
56

67
# Adapter build artifacts
78
adapters/python/pardus-playwright/*.egg-info/
@@ -13,4 +14,24 @@ adapters/node/pardus-puppeteer/dist/
1314
adapters/node/pardus-playwright/node_modules/
1415
adapters/node/pardus-playwright/dist/
1516

16-
.env
17+
# Tauri frontend
18+
crates/pardus-tauri/node_modules/
19+
crates/pardus-tauri/dist/
20+
21+
# Web dashboard
22+
web/node_modules/
23+
web/dist/
24+
25+
# AI agent
26+
ai-agent/pardus-browser/node_modules/
27+
ai-agent/pardus-browser/dist/
28+
29+
# Lockfiles
30+
**/package-lock.json
31+
32+
.env
33+
.env.*
34+
!.env.example
35+
36+
# Research benchmark data
37+
research/

Cargo.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,12 @@
22
members = [
33
"crates/pardus-core",
44
"crates/pardus-cdp",
5+
"crates/pardus-challenge",
56
"crates/pardus-cli",
67
"crates/pardus-debug",
78
"crates/pardus-kg",
9+
"crates/pardus-server",
10+
"crates/pardus-tauri/src-tauri",
811
]
912
resolver = "2"
1013

@@ -27,6 +30,8 @@ futures-util = "0.3"
2730
blake3 = "1"
2831
lol_html = "2"
2932
reqwest = { version = "0.12", features = ["cookies", "gzip", "brotli", "deflate", "json"] }
33+
rquest = { version = "5", features = ["cookies", "gzip", "brotli", "deflate", "json", "stream", "socks"] }
34+
rquest-util = "2"
3035
parking_lot = "0.12"
3136
base64 = "0.22"
3237
async-trait = "0.1"

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ rustup install nightly
5454
# Clone and build
5555
git clone https://github.com/user/pardus-browser.git
5656
cd pardus-browser
57-
cargo +nightly install --path crates/pardus-cli --feature js
57+
cargo +nightly install --path crates/pardus-cli --features js
5858
```
5959

6060
### Docker

ROADMAP.md

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -39,10 +39,10 @@ _(Currently empty)_
3939
## Planned (Near-term)
4040

4141
### Screenshots (Optional)
42-
- [ ] HTML→PNG rendering — For when pixels actually matter
43-
- [ ] Element screenshots — Capture specific element bounds
44-
- [ ] Viewport clipping — Configurable resolution
45-
- [ ] CDP screenshot API — Page.captureScreenshot compliance
42+
- [x] HTML→PNG rendering — For when pixels actually matter
43+
- [x] Element screenshots — Capture specific element bounds
44+
- [x] Viewport clipping — Configurable resolution
45+
- [x] CDP screenshot API — Page.captureScreenshot compliance
4646

4747
---
4848

@@ -79,17 +79,17 @@ _(Currently empty)_
7979

8080
### Network & Protocol
8181

82-
- [ ] **Request interception** — Intercept, modify, or block requests before they're sent (URL rewrite, header injection, body substitution)
83-
- [ ] **Response mocking** — Return canned responses for specific URL patterns; useful for testing agents against controlled data
84-
- [ ] **Request deduplication** — Avoid parallel fetches of the same resource within a time window
85-
- [ ] **Retry with backoff** — Configurable retry policy for transient failures (5xx, timeout, connection reset)
86-
- [ ] **Cookie jar API** — Full programmatic cookie management (list, set, delete, domain filtering) via CLI, CDP, and library
82+
- [x] **Request interception** — Intercept, modify, or block requests before they're sent (URL rewrite, header injection, body substitution)
83+
- [x] **Response mocking** — Return canned responses for specific URL patterns; useful for testing agents against controlled data
84+
- [x] **Request deduplication** — Avoid parallel fetches of the same resource within a time window
85+
- [x] **Retry with backoff** — Configurable retry policy for transient failures (5xx, timeout, connection reset)
86+
- [x] **Cookie jar API** — Full programmatic cookie management (list, set, delete, domain filtering) via CLI, CDP, and library
8787
- [ ] **Auth token rotation** — Auto-refresh expiring Bearer tokens when 401 is received; configurable refresh endpoint/callback
8888

8989
### Web Standards & Content
9090

91-
- [ ] **PDF text extraction** — Parse PDF bytes to semantic tree (already partially implemented in `pdf.rs`); extend with table, form-field, and image extraction
92-
- [ ] **RSS/Atom feed parsing** — Detect and parse feed content into structured items (title, link, date, summary)
91+
- [x] **PDF text extraction** — Parse PDF bytes to semantic tree with table, form-field (AcroForm), and image metadata extraction
92+
- [x] **RSS/Atom feed parsing** — Detect and parse RSS/Atom feed content into structured items (title, link, date, summary)
9393
- [ ] **Robots.txt parser** — Respect crawl directives; expose `is_allowed(url)` for the knowledge graph crawler
9494
- [ ] **Meta refresh & redirects** — Parse `<meta http-equiv="refresh">` and JS `location.href` assignments as navigations
9595
- [ ] **Content encoding** — Handle gzip/brotli/zstd transfer encodings beyond what reqwest provides automatically
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
import { describe, it, beforeEach, afterEach } from 'node:test';
2+
import assert from 'node:assert';
3+
import { CookieStore } from '../../core/CookieStore.js';
4+
import { mkdirSync, writeFileSync, rmSync, existsSync } from 'node:fs';
5+
import { join } from 'node:path';
6+
import { tmpdir } from 'node:os';
7+
8+
describe('CookieStore', () => {
9+
let store: CookieStore;
10+
let testDir: string;
11+
12+
beforeEach(() => {
13+
testDir = join(tmpdir(), `pardus-test-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`);
14+
mkdirSync(testDir, { recursive: true });
15+
store = new CookieStore(testDir);
16+
});
17+
18+
afterEach(() => {
19+
if (existsSync(testDir)) {
20+
rmSync(testDir, { recursive: true, force: true });
21+
}
22+
});
23+
24+
describe('saveCookies / loadCookies', () => {
25+
it('should save and load cookies round-trip', () => {
26+
const cookies = [
27+
{ name: 'session', value: 'abc123', domain: '.example.com', path: '/', sameSite: 'Lax' as const },
28+
{ name: 'token', value: 'xyz789', domain: '.example.com', path: '/api', secure: true, httpOnly: true },
29+
];
30+
31+
store.saveCookies('test-profile', cookies);
32+
33+
const loaded = store.loadCookies('test-profile');
34+
assert.strictEqual(loaded.length, 2);
35+
assert.strictEqual(loaded[0].name, 'session');
36+
assert.strictEqual(loaded[0].value, 'abc123');
37+
assert.strictEqual(loaded[0].domain, '.example.com');
38+
assert.strictEqual(loaded[1].name, 'token');
39+
assert.strictEqual(loaded[1].secure, true);
40+
assert.strictEqual(loaded[1].httpOnly, true);
41+
});
42+
43+
it('should return empty array for non-existent profile', () => {
44+
const loaded = store.loadCookies('non-existent');
45+
assert.deepStrictEqual(loaded, []);
46+
});
47+
48+
it('should return empty array for empty profile name', () => {
49+
const loaded = store.loadCookies('');
50+
assert.deepStrictEqual(loaded, []);
51+
});
52+
53+
it('should overwrite existing cookies on save', () => {
54+
store.saveCookies('test', [{ name: 'a', value: '1' }]);
55+
store.saveCookies('test', [{ name: 'b', value: '2' }, { name: 'c', value: '3' }]);
56+
57+
const loaded = store.loadCookies('test');
58+
assert.strictEqual(loaded.length, 2);
59+
assert.strictEqual(loaded[0].name, 'b');
60+
});
61+
62+
it('should not save empty cookie array', () => {
63+
store.saveCookies('test', []);
64+
65+
const loaded = store.loadCookies('test');
66+
assert.deepStrictEqual(loaded, []);
67+
});
68+
69+
it('should not save when profile is empty', () => {
70+
store.saveCookies('', [{ name: 'a', value: '1' }]);
71+
72+
const loaded = store.loadCookies('');
73+
assert.deepStrictEqual(loaded, []);
74+
});
75+
76+
it('should create the profile directory if it does not exist', () => {
77+
store.saveCookies('new-profile', [{ name: 'test', value: 'val' }]);
78+
79+
const loaded = store.loadCookies('new-profile');
80+
assert.strictEqual(loaded.length, 1);
81+
});
82+
});
83+
84+
describe('corrupt data handling', () => {
85+
it('should return empty array for corrupt JSON', () => {
86+
const profileDir = join(testDir, 'corrupt');
87+
mkdirSync(profileDir, { recursive: true });
88+
writeFileSync(join(profileDir, 'cookies.json'), 'not valid json{{{');
89+
90+
const loaded = store.loadCookies('corrupt');
91+
assert.deepStrictEqual(loaded, []);
92+
});
93+
94+
it('should return empty array when cookies field is missing', () => {
95+
const profileDir = join(testDir, 'no-cookies');
96+
mkdirSync(profileDir, { recursive: true });
97+
writeFileSync(join(profileDir, 'cookies.json'), JSON.stringify({ savedAt: Date.now() }));
98+
99+
const loaded = store.loadCookies('no-cookies');
100+
assert.deepStrictEqual(loaded, []);
101+
});
102+
});
103+
104+
describe('deleteProfile', () => {
105+
it('should delete an existing profile', () => {
106+
store.saveCookies('to-delete', [{ name: 'a', value: '1' }]);
107+
108+
assert.ok(store.loadCookies('to-delete').length > 0);
109+
store.deleteProfile('to-delete');
110+
111+
const loaded = store.loadCookies('to-delete');
112+
assert.deepStrictEqual(loaded, []);
113+
});
114+
115+
it('should not throw for non-existent profile', () => {
116+
assert.doesNotThrow(() => store.deleteProfile('non-existent'));
117+
});
118+
});
119+
120+
describe('listProfiles', () => {
121+
it('should list profiles that have cookies', () => {
122+
store.saveCookies('profile-a', [{ name: 'a', value: '1' }]);
123+
store.saveCookies('profile-b', [{ name: 'b', value: '2' }]);
124+
125+
const profiles = store.listProfiles();
126+
assert.ok(profiles.includes('profile-a'));
127+
assert.ok(profiles.includes('profile-b'));
128+
assert.strictEqual(profiles.length, 2);
129+
});
130+
131+
it('should not list profiles with empty cookies (not saved)', () => {
132+
store.saveCookies('empty', []);
133+
134+
const profiles = store.listProfiles();
135+
assert.ok(!profiles.includes('empty'));
136+
});
137+
138+
it('should return empty array when no profiles exist', () => {
139+
const profiles = store.listProfiles();
140+
assert.deepStrictEqual(profiles, []);
141+
});
142+
});
143+
});

ai-agent/pardus-browser/src/__tests__/llm/prompts.test.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,15 @@ describe('Prompts', () => {
2929
assert.ok(SYSTEM_PROMPT.includes('browser_close'));
3030
assert.ok(SYSTEM_PROMPT.includes('browser_list'));
3131
assert.ok(SYSTEM_PROMPT.includes('browser_get_state'));
32+
assert.ok(SYSTEM_PROMPT.includes('browser_get_action_plan'));
33+
assert.ok(SYSTEM_PROMPT.includes('browser_auto_fill'));
34+
assert.ok(SYSTEM_PROMPT.includes('browser_wait'));
35+
assert.ok(SYSTEM_PROMPT.includes('browser_get_cookies'));
36+
assert.ok(SYSTEM_PROMPT.includes('browser_set_cookie'));
37+
});
38+
39+
it('should mention correct tool count', () => {
40+
assert.ok(SYSTEM_PROMPT.includes('19 browser tools'));
3241
});
3342

3443
it('should have workflow steps', () => {

ai-agent/pardus-browser/src/__tests__/test-utils.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,36 @@ export function createMockBrowserManager(): BrowserManager {
106106
title: 'Example',
107107
markdown: ''
108108
}),
109+
getActionPlan: async () => ({
110+
success: true,
111+
actionPlan: {
112+
url: instanceUrls.get(id) || 'https://example.com',
113+
suggestions: [
114+
{ action_type: 'Click', reason: 'Submit button found', confidence: 0.95, selector: 'button[type="submit"]' },
115+
{ action_type: 'Fill', reason: 'Empty email field', confidence: 0.9, selector: 'input[name="email"]', label: 'Email' },
116+
],
117+
page_type: 'FormPage',
118+
has_forms: true,
119+
has_pagination: false,
120+
interactive_count: 5,
121+
},
122+
}),
123+
autoFill: async () => ({
124+
success: true,
125+
filledFields: [
126+
{ field_name: 'email', value: 'test@example.com', matched_by: 'ByName' },
127+
{ field_name: 'password', value: 'secret123', matched_by: 'ByType' },
128+
],
129+
unmatchedFields: [
130+
{ field_type: 'text', label: 'Phone', placeholder: 'Enter phone', required: false, field_name: 'phone' },
131+
],
132+
}),
133+
wait: async (condition: string) => ({
134+
success: true,
135+
satisfied: true,
136+
condition,
137+
reason: condition === 'contentLoaded' ? 'content-loaded' : 'content-stable',
138+
}),
109139
kill: () => {},
110140
on: function(event: string, handler: () => void) {
111141
return this;

0 commit comments

Comments
 (0)