qq-cli 0.1.0

A modular Rust QQ task interface for coding agents
#![cfg(windows)]
#[path = "common/login_fixture.rs"]
mod fixture;
#[path = "common/login_service.rs"]
mod service;
use fixture::{Fixture, wait};
use qq_cli::storage::credentials::Credentials;
#[test]
fn pending_status_observes_control_but_only_onebot_confirms_actual_account() {
    if service::child() {
        return;
    }
    let fixture =
        Fixture::new("pending_status_observes_control_but_only_onebot_confirms_actual_account");
    let before = std::fs::read(fixture.store.root().join("registry.json")).unwrap();
    for (mode, phase) in [("", "qrcode_scanned"), ("control-ready", "ready")] {
        fixture.mode(mode);
        let (code, value) = fixture.status();
        assert_eq!(code, 0, "{value}");
        assert_eq!(value["data"]["state"], "login_required");
        assert_eq!(value["data"]["identityVerified"], false);
        assert!(value["data"]["account"].is_null());
        assert!(value.get("meta").is_none());
        assert_eq!(value["data"]["loginObservation"]["phase"], phase);
        assert_eq!(
            std::fs::read(fixture.store.root().join("registry.json")).unwrap(),
            before
        );
    }
    assert_eq!(fixture.counts()["auth"], 2);
    assert_eq!(fixture.counts()["control"], 2);
    assert_eq!(fixture.counts()["api"], 0);
    fixture.mode("api-pending");
    std::fs::write(
        fixture.temp.path().join("api-ready"),
        b"original API enable",
    )
    .unwrap();
    wait(&fixture.temp.path().join("api-listening"));
    let (code, value) = fixture.status();
    assert_eq!(code, 0, "{value}");
    assert_eq!(value["data"]["loginObservation"]["phase"], "initializing");
    assert!(value["data"]["account"].is_null());
    assert_eq!(
        std::fs::read(fixture.store.root().join("registry.json")).unwrap(),
        before
    );
    fixture.mode("bad-version");
    let (code, error) = fixture.status();
    assert_eq!(code, 3, "{error}");
    assert_eq!(error["error"]["code"], "VERSION_MISMATCH");
    let calls = fixture.counts()["api"].as_u64().unwrap();
    fixture.mode("bad-version-pending");
    let (code, error) = fixture.status();
    assert_eq!(code, 3, "{error}");
    assert_eq!(error["error"]["code"], "VERSION_MISMATCH");
    assert_eq!(fixture.counts()["api"].as_u64().unwrap(), calls + 1);
    assert_eq!(fixture.counts()["auth"], 3);
    fixture.mode("api-malformed");
    let (code, error) = fixture.status();
    assert_eq!(code, 3, "{error}");
    assert_eq!(error["error"]["code"], "BACKEND_TRANSPORT");
    assert_eq!(fixture.counts()["auth"], 3);
    fixture.mode("api-bad-identity");
    let (code, error) = fixture.status();
    assert_eq!(code, 3, "{error}");
    assert_eq!(error["error"]["code"], "BACKEND_PROTOCOL");
    assert_eq!(fixture.counts()["auth"], 3);
    assert_eq!(
        std::fs::read(fixture.store.root().join("registry.json")).unwrap(),
        before
    );
    fixture.mode("");
    let (code, value) = fixture.status();
    assert_eq!(code, 0, "{value}");
    assert_eq!(value["data"]["state"], "ready");
    assert_eq!(value["meta"]["scope"]["account"], "10001");
    assert!(value["data"].get("loginObservation").is_none());
    let actual = fixture.store.read().unwrap();
    assert_eq!(actual.instances[0].state, "ready");
    assert_eq!(actual.instances[0].account.as_deref(), Some("10001"));
    assert_eq!(fixture.counts()["auth"], 3);
}

#[test]
fn control_failures_and_concurrent_run_change_are_not_unverified_successes() {
    if service::child() {
        return;
    }
    let fixture =
        Fixture::new("control_failures_and_concurrent_run_change_are_not_unverified_successes");
    for (mode, expected) in [
        ("2fa", "CONTROL_2FA_REQUIRED"),
        ("reject", "CONTROL_REJECTED"),
    ] {
        fixture.mode(mode);
        let (code, error) = fixture.status();
        assert_ne!(code, 0);
        assert_eq!(error["error"]["code"], expected);
        assert_eq!(fixture.counts()["control"], 0);
    }
    fixture.mode("race");
    let (code, error) = fixture.status();
    assert_eq!(code, 4, "{error}");
    assert_eq!(error["error"]["code"], "STATUS_INPUT_CHANGED");
    assert_eq!(fixture.store.read().unwrap().instances[0].account, None);
    assert_eq!(fixture.counts()["auth"], 3);
    assert_eq!(fixture.counts()["control"], 1);
}

#[test]
fn missing_original_control_material_is_not_repaired_or_sent_to_the_backend() {
    if service::child() {
        return;
    }
    let fixture =
        Fixture::new("missing_original_control_material_is_not_repaired_or_sent_to_the_backend");
    let (code, value) = fixture.status();
    assert_eq!(code, 0, "{value}");
    fixture.mode("api-pending");
    std::fs::write(
        fixture.temp.path().join("api-ready"),
        b"original API enabled",
    )
    .unwrap();
    wait(&fixture.temp.path().join("api-listening"));
    let (code, value) = fixture.status();
    assert_eq!(code, 0, "{value}");
    assert_eq!(fixture.counts()["auth"], 2);
    std::fs::write(fixture.temp.path().join("api-stop"), b"original API stop").unwrap();
    wait(&fixture.temp.path().join("api-stopped"));
    let (code, value) = fixture.status();
    assert_eq!(code, 0, "{value}");
    assert!(value["data"]["account"].is_null());
    assert_eq!(fixture.counts()["auth"], 3);
    for directory in ["retentions", "runtime-preparations"] {
        let file = std::fs::read_dir(fixture.store.root().join(directory))
            .unwrap()
            .next()
            .unwrap()
            .unwrap()
            .path();
        let original = std::fs::read(&file).unwrap();
        std::fs::remove_file(&file).unwrap();
        let (code, error) = fixture.status();
        assert_ne!(code, 0, "{error}");
        assert!(!file.exists());
        assert_eq!(fixture.counts()["auth"], 3);
        std::fs::write(&file, original).unwrap();
    }
    let key = &fixture.keys[1];
    let original = fixture.credentials.get(key).unwrap();
    fixture.credentials.delete(key).unwrap();
    let (code, error) = fixture.status();
    assert_ne!(code, 0, "{error}");
    assert!(fixture.credentials.get(key).is_err());
    assert_eq!(fixture.counts()["auth"], 3);
    fixture.credentials.set(key, &original).unwrap();
    let (code, value) = fixture.status();
    assert_eq!(code, 0, "{value}");
    assert_eq!(fixture.counts()["auth"], 4);
}