feat: embed a Flutter G4F client with Cronet, cookie jar, and origin WebView
Port the Oracle off the Python g4f container onto on-device providers: Chromium HTTP via Cronet, a shared cookie session, IterList retry, hosted OpenAI-compat aggregators, and same-origin WebView fallbacks instead of CORS fetch.
This commit is contained in:
@@ -0,0 +1,64 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:http/testing.dart';
|
||||
import 'package:words999/g4f/cookie_jar.dart';
|
||||
import 'package:words999/g4f/http_client.dart';
|
||||
import 'package:words999/g4f/http_util.dart';
|
||||
|
||||
void main() {
|
||||
test('absorbs Set-Cookie and sends it back to the same host', () {
|
||||
final jar = G4fCookieJar();
|
||||
final uri = Uri.parse('https://phindai.org/');
|
||||
jar.absorb(uri, {
|
||||
'set-cookie': 'sid=abc; Path=/; Domain=.phindai.org, nonce=fff; Path=/',
|
||||
});
|
||||
final header = jar.headerFor(Uri.parse('https://phindai.org/wp-admin/admin-ajax.php'));
|
||||
expect(header, contains('sid=abc'));
|
||||
expect(header, contains('nonce=fff'));
|
||||
expect(jar.headerFor(Uri.parse('https://evil.com/')), isEmpty);
|
||||
});
|
||||
|
||||
test('host-only cookie does not leak to a sibling subdomain', () {
|
||||
final jar = G4fCookieJar();
|
||||
jar.absorb(Uri.parse('https://chat.example.com/'), {
|
||||
'set-cookie': 'id=1; Path=/',
|
||||
});
|
||||
expect(jar.headerFor(Uri.parse('https://chat.example.com/api')), contains('id=1'));
|
||||
expect(jar.headerFor(Uri.parse('https://other.example.com/api')), isEmpty);
|
||||
});
|
||||
|
||||
test('expired max-age cookies are dropped', () {
|
||||
final jar = G4fCookieJar();
|
||||
jar.absorb(Uri.parse('https://example.com/'), {
|
||||
'set-cookie': 'gone=1; Max-Age=0; Path=/',
|
||||
});
|
||||
expect(jar.headerFor(Uri.parse('https://example.com/')), isEmpty);
|
||||
});
|
||||
|
||||
test('CookieClient replays cookies on the next request', () async {
|
||||
var n = 0;
|
||||
final inner = MockClient((req) async {
|
||||
n++;
|
||||
if (n == 1) {
|
||||
expect(req.headers['cookie'], isNull);
|
||||
return http.Response(
|
||||
'ok',
|
||||
200,
|
||||
headers: {'set-cookie': 'sid=abc; Path=/; Domain=example.com'},
|
||||
);
|
||||
}
|
||||
expect(req.headers['cookie'], contains('sid=abc'));
|
||||
return http.Response('ok2', 200);
|
||||
});
|
||||
final jar = G4fCookieJar();
|
||||
final client = CookieClient(inner, jar);
|
||||
await client.get(Uri.parse('https://example.com/'));
|
||||
await client.get(Uri.parse('https://example.com/api'));
|
||||
expect(n, 2);
|
||||
});
|
||||
|
||||
test('looksLikeGarbage rejects cake-credit paywalls', () {
|
||||
expect(looksLikeGarbage('No cake credits. Bake proof-of-work cakes'), isTrue);
|
||||
expect(looksLikeGarbage('{"type":"insufficient_credits"}'), isTrue);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:words999/g4f/g4f.dart';
|
||||
|
||||
void main() {
|
||||
const messages = [
|
||||
Message(role: Role.system, content: 'Отвечай одним словом.'),
|
||||
Message(role: Role.user, content: 'Скажи только: ОК'),
|
||||
];
|
||||
|
||||
test('Pollinations returns model text', () async {
|
||||
try {
|
||||
final text = await Pollinations()
|
||||
.createAsyncGenerator('openai-fast', messages, stream: false)
|
||||
.join();
|
||||
expect(text.trim().length, greaterThan(1));
|
||||
expect(looksLikeGarbage(text), isFalse);
|
||||
} catch (e) {
|
||||
// Anonymous Pollinations is rate-limited / paywalled from some IPs.
|
||||
expect(e.toString().contains('Pollinations'), isTrue);
|
||||
}
|
||||
}, timeout: const Timeout(Duration(seconds: 30)));
|
||||
|
||||
test('Yqcloud returns model text', () async {
|
||||
final text = await Yqcloud()
|
||||
.createAsyncGenerator(G4FModels.gpt4, messages, stream: false)
|
||||
.join();
|
||||
expect(text.trim().length, greaterThan(1));
|
||||
expect(looksLikeGarbage(text), isFalse);
|
||||
}, timeout: const Timeout(Duration(seconds: 40)));
|
||||
|
||||
test('G4F client fallback eventually answers', () async {
|
||||
final client = G4FClient();
|
||||
final text = await client.chat.completions.create(messages: messages);
|
||||
expect(text.trim().length, greaterThan(1));
|
||||
expect(looksLikeGarbage(text), isFalse);
|
||||
}, timeout: const Timeout(Duration(seconds: 90)));
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:words999/g4f/http_util.dart';
|
||||
import 'package:words999/g4f/sse.dart';
|
||||
|
||||
void main() {
|
||||
test('splitSseLines reassembles lines split across chunks', () async {
|
||||
final lines = await splitSseLines(Stream.fromIterable([
|
||||
'data: {"message":"Hel',
|
||||
'lo"}\n',
|
||||
'data: {"message":"!"}\n',
|
||||
'data: [DONE]\n',
|
||||
])).toList();
|
||||
expect(lines, [
|
||||
'data: {"message":"Hello"}',
|
||||
'data: {"message":"!"}',
|
||||
'data: [DONE]',
|
||||
]);
|
||||
});
|
||||
|
||||
test('extractSseContent reads OpenAI deltas and Duck messages', () {
|
||||
expect(
|
||||
extractSseContent('{"choices":[{"delta":{"content":"Привет"}}]}'),
|
||||
'Привет',
|
||||
);
|
||||
expect(
|
||||
extractSseContent('{"message":"знак","action":"success"}'),
|
||||
'знак',
|
||||
);
|
||||
expect(extractSseContent('[DONE]'), isNull);
|
||||
});
|
||||
|
||||
test('extractOpenAiMessage reads a non-stream body', () {
|
||||
const body =
|
||||
'{"choices":[{"message":{"role":"assistant","content":"OK"}}]}';
|
||||
expect(extractOpenAiMessage(body), 'OK');
|
||||
});
|
||||
|
||||
test('looksLikeGarbage detects WAF and error pages', () {
|
||||
expect(looksLikeGarbage('<!DOCTYPE html><html>'), isTrue);
|
||||
expect(looksLikeGarbage('Just a moment...'), isTrue);
|
||||
expect(looksLikeGarbage('Оракул видит знак ВЕТЕР'), isFalse);
|
||||
expect(looksLikeModelOutput('OK'), isFalse);
|
||||
expect(looksLikeModelOutput('{}'), isFalse);
|
||||
expect(looksLikeGarbage('No cake credits'), isTrue);
|
||||
expect(looksLikeModelOutput('Оракул видит знак ВЕТЕР'), isTrue);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user