<commit_before>var _ = require("lodash");
var test = require("tape");
var cocb = require("co-callback");
var ktypes = require("krl-stdlib/types");
var strictDeepEquals = require("krl-stdlib/strictEquals").strictDeepEquals;
var kengine = require("./engine");
var mkTestPicoEngine = require("../mkTestPicoEngine");

//wrap stubbed functions in this to simulate async
var tick = function(fn){
    return function(){
        var args = _.toArray(arguments);
        process.nextTick(function(){
            fn.apply(null, args);
        });
    };
};

var runAction = function*(pe, ctx, domain, id, args){
    var act = yield pe.modules.get(ctx, domain, id);
    return _.head(yield act(ctx, args));
};


var testPE = function(test_name, genfn){
    test(test_name, function(t){
        mkTestPicoEngine({
            rootRIDs: ["io.picolabs.engine"],
        }, function(err, pe){
            if(err) return t.end(err);

            cocb.run(function*(){
                yield genfn(t, pe);
            }, t.end);
        });
    });
};


var testErr = function*(t, genfn, failMsg, errMsg){
    try{
        yield genfn;
        t.fail(failMsg);
    }catch(err){
        t.equals(err + "", errMsg);
    }
};

var assertPicoID = function(id, callback){
    if( ! _.isString(id)){
        return callback(new Error("Invalid pico_id: " + ktypes.toString(id)));
    }
    callback(null, id);
};


test("engine:getPicoIDByECI", function(t){
    cocb.run(function*(){
        var engine = kengine({
            db: {
                getPicoIDByECI: tick(function(eci, callback){
                    if(eci === "foo"){
                        return callback(null, "bar");
                    }else if(eci === "baz"){
                        return callback(null, "qux");
                    }
                    callback("NOT FOUND:" + eci);
                })
            }
        });
        var get = function*(eci){
            var eciGiven = eci !== void 0;
            return yield engine.def.getPicoIDByECI(
                {},
                eciGiven ? {eci: eci,} : {}
            );
        };

        t.equals(yield get("foo"), "bar");
        t.equals(yield get("baz"), "qux");

        yield testErr(
            t,
            get(),
            "should throw b/c no eci is given",
            "Error: engine:getPicoIDByECI needs an eci string"
        );
        yield testErr(
            t,
            get(null),
            "should throw b/c wrong eci type",
            "TypeError: engine:getPicoIDByECI was given null instead of an eci string"
        );
        yield testErr(
            t,
            get("quux"),
            "should throw b/c not found",
            "NOT FOUND:quux"
        );

    }, t.end);
});


test("engine:registerRuleset", function(t){
    cocb.run(function*(){

        var engine = kengine({
            registerRulesetURL: tick(function(url, callback){
                callback(null, {
                    rid: "rid for: " + url
                });
            })
        });

        t.equals((yield engine.def.registerRuleset({}, {
            url: "http://foo.bar/qux.krl",
        }))[0], "rid for: http://foo.bar/qux.krl");

        t.equals((yield engine.def.registerRuleset({}, {
            url: "qux.krl",
            base: "https://foo.bar/baz/",
        }))[0], "rid for: https://foo.bar/baz/qux.krl");

        yield testErr(
            t,
            engine.def.registerRuleset({}, []),
            "should throw b/c no url is given",
            "Error: engine:registerRuleset needs a url string"
        );

        yield testErr(
            t,
            engine.def.registerRuleset({}, [_.noop]),
            "should throw b/c wrong url type",
            "TypeError: engine:registerRuleset was given [Function] instead of a url string"
        );

    }, t.end);
});

test("engine:installRuleset", function(t){
    cocb.run(function*(){
        var engine = kengine({
            installRuleset: tick(function(pico_id, rid, callback){
                callback();
            }),
            registerRulesetURL: tick(function(url, callback){
                callback(null, {
                    rid: "REG:" + /\/([^/]*)\.krl$/.exec(url)[1]
                });
            }),
            db: {
                assertPicoID: assertPicoID,
                findRulesetsByURL: tick(function(url, callback){
                    if(url === "http://foo.bar/baz/qux.krl"){
                        return callback(null, [{rid: "found"}]);
                    }else if(url === "file:///too/many.krl"){
                        return callback(null, [{rid: "a"}, {rid: "b"}, {rid: "c"}]);
                    }
                    callback(null, []);
                }),
            }
        });

        var inst = function*(id, rid, url, base){
            var args = {};
            if(id !== void 0){
                args.pico_id = id;
            }
            if(rid !== void 0){
                args.rid = rid;
            }
            if(url !== void 0){
                args.url = url;
            }
            if(base !== void 0){
                args.base = base;
            }
            return (yield engine.def.installRuleset({}, args))[0];
        };

        t.equals(yield inst("pico0", "foo.bar"), "foo.bar");
        t.deepEquals(yield inst("pico0", ["foo.bar", "foo.qux"]), ["foo.bar", "foo.qux"]);
        strictDeepEquals(t, yield inst("pico0", []), []);
        t.deepEquals(yield inst("pico0", void 0, "file:///foo/bar.krl"), "REG:bar");
        t.deepEquals(yield inst("pico0", void 0, "qux.krl", "http://foo.bar/baz/"), "found");

        yield testErr(
            t,
            inst("pico0", void 0, "file:///too/many.krl"),
            "should throw b/c too many matched",
            "Error: More than one rid found for the given url: a , b , c"
        );

    }, t.end);
});

test("engine:uninstallRuleset", function(t){
    cocb.run(function*(){

        var uninstalled = {};
        var order = 0;

        var engine = kengine({
            uninstallRuleset: tick(function(id, rid, callback){
                if(id !== "pico0"){
                    return callback(new Error("invalid pico_id"));
                }
                if(!_.isString(rid)){
                    return callback(new Error("invalid rid"));
                }
                _.set(uninstalled, [id, rid], order++);
                callback();
            }),
            db: {
                assertPicoID: assertPicoID,
            }
        });

        t.equals((yield engine.def.uninstallRuleset({}, {
            pico_id: "pico0",
            rid: "foo.bar",
        }))[0], void 0);

        t.equals((yield engine.def.uninstallRuleset({}, {
            pico_id: "pico0",
            rid: ["baz", "qux"],
        }))[0], void 0);

        t.deepEquals(uninstalled, {
            pico0: {
                "foo.bar": 0,
                "baz": 1,
                "qux": 2,
            }
        });

    }, t.end);
});

test("engine:unregisterRuleset", function(t){
    cocb.run(function*(){
        var log = [];
        var engine = kengine({
            unregisterRuleset: tick(function(rid, callback){
                if(!_.isString(rid)){
                    return callback("invalid rid");
                }
                log.push(rid);
                callback();
            }),
        });

        t.equals((yield engine.def.unregisterRuleset({}, {
            rid: "foo.bar",
        }))[0], void 0);

        t.equals((yield engine.def.unregisterRuleset({}, {
            rid: ["baz", "qux"],
        }))[0], void 0);

        yield testErr(
            t,
            engine.def.unregisterRuleset({}, []),
            void 0,
            "Error: engine:unregisterRuleset needs a rid string or array"
        );

        yield testErr(
            t,
            engine.def.unregisterRuleset({}, {rid: {},}),
            void 0,
            "TypeError: engine:unregisterRuleset was given [Map] instead of a rid string or array"
        );

        yield testErr(
            t,
            engine.def.unregisterRuleset({}, {
                rid: ["baz", 2, "qux"],
            }),
            void 0,
            "TypeError: engine:unregisterRuleset was given a rid array containing a non-string (2)"
        );

        t.deepEquals(log, [
            "foo.bar",
            "baz",
            "qux",
        ]);

    }, t.end);
});

testPE("engine:describeRuleset", function * (t, pe){
    var ctx = {};
    var descRID = yield pe.modules.get(ctx, "engine", "describeRuleset");

    var desc = yield descRID(ctx, {rid: "io.picolabs.hello_world"});

    var isIsoString = function(str){
        return str === (new Date(str)).toISOString();
    };

    t.deepEquals(_.keys(desc), [
        "rid",
        "src",
        "hash",
        "url",
        "timestamp_stored",
        "timestamp_enable",
        "meta",
    ]);
    t.equals(desc.rid, "io.picolabs.hello_world");
    t.ok(_.isString(desc.src));
    t.ok(_.isString(desc.hash));
    t.ok(_.isString(desc.url));
    t.ok(isIsoString(desc.timestamp_stored));
    t.ok(isIsoString(desc.timestamp_enable));
    t.deepEquals(desc.meta, {
        name: "<NAME>",
        description: "\nA first ruleset for the Quickstart\n        ",
        author: "<NAME>",
    });

    yield testErr(
        t,
        descRID(ctx, []),
        "should throw b/c no rid is given",
        "Error: engine:describeRuleset needs a rid string"
    );
    yield testErr(
        t,
        descRID(ctx, [[]]),
        "should throw b/c wrong rid type",
        "TypeError: engine:describeRuleset was given [Array] instead of a rid string"
    );
    try{
        yield descRID(ctx, {rid: "not.found"});
        t.fail("should fail b/c not found");
    }catch(err){
        t.ok(err && err.notFound);
    }
});

testPE("engine:listInstalledRIDs", function * (t, pe){
    var ctx = {};
    var listRIDs = yield pe.modules.get(ctx, "engine", "listInstalledRIDs");

    var rids = yield listRIDs(ctx, {pico_id: "id0"});

    t.deepEquals(rids, [
        "io.picolabs.engine",
    ]);
});

//TODO testPE("engine:listAllEnabledRIDs...

testPE("engine:newPico", function * (t, pe){
    var action = function*(ctx, name, args){
        return yield runAction(pe, ctx, "engine", name, args);
    };

    var pico2 = yield action({}, "newPico", {
        parent_id: "id0",
    });
    t.deepEquals(pico2, {
        id: "id2",
        parent_id: "id0",
        admin_eci: "id3",
    });

    //default to ctx.pico_id
    var pico3 = yield action({
        pico_id: "id2",//called by pico2
    }, "newPico", {});
    t.deepEquals(pico3, {
        id: "id4",
        parent_id: "id2",
        admin_eci: "id5",
    });
});


testPE("engine:getParent, engine:getAdminECI, engine:listChildren, engine:removePico", function * (t, pe){

    var newPico = function*(ctx, args){
        return yield runAction(pe, ctx, "engine", "newPico", args);
    };
    var removePico = function*(ctx, args){
        return yield runAction(pe, ctx, "engine", "removePico", args);
    };

    var getParent = yield pe.modules.get({}, "engine", "getParent");
    var getAdminECI = yield pe.modules.get({}, "engine", "getAdminECI");
    var listChildren = yield pe.modules.get({}, "engine", "listChildren");

    yield newPico({pico_id: "id0"}, []);// id2
    yield newPico({}, ["id0"]);// id4
    yield newPico({pico_id: "id2"}, []);// id6

    t.equals(yield getParent({}, ["id0"]), null);
    t.equals(yield getParent({}, ["id2"]), "id0");
    t.equals(yield getParent({}, ["id4"]), "id0");
    t.equals(yield getParent({}, ["id6"]), "id2");

    t.equals(yield getAdminECI({}, ["id0"]), "id1");
    t.equals(yield getAdminECI({}, ["id2"]), "id3");
    t.equals(yield getAdminECI({}, ["id4"]), "id5");
    t.equals(yield getAdminECI({}, ["id6"]), "id7");

    t.deepEquals(yield listChildren({}, ["id0"]), ["id2", "id4"]);
    t.deepEquals(yield listChildren({}, ["id2"]), ["id6"]);
    strictDeepEquals(t, yield listChildren({}, ["id4"]), []);
    strictDeepEquals(t, yield listChildren({}, ["id6"]), []);

    //fallback on ctx.pico_id
    t.equals(yield getParent({pico_id: "id6"}, []), "id2");
    t.equals(yield getAdminECI({pico_id: "id6"}, []), "id7");
    t.deepEquals(yield listChildren({pico_id: "id2"}, []), ["id6"]);
    t.equals(yield removePico({pico_id: "id6"}, []), void 0);
    strictDeepEquals(t, yield listChildren({}, ["id2"]), []);

    //report error on invalid pico_id
    var assertInvalidPicoID = function * (genfn, id, expected){
        yield testErr(
            t,
            genfn({pico_id: id}, []),
            void 0,
            expected
        );
    };

    yield assertInvalidPicoID(getParent   , void 0, "TypeError: engine:getParent was given null instead of a pico_id string");
    yield assertInvalidPicoID(getAdminECI , void 0, "TypeError: engine:getAdminECI was given null instead of a pico_id string");
    yield assertInvalidPicoID(listChildren, void 0, "TypeError: engine:listChildren was given null instead of a pico_id string");
    yield assertInvalidPicoID(newPico     , void 0, "TypeError: engine:newPico was given null instead of a parent_id string");
    yield assertInvalidPicoID(removePico  , void 0, "TypeError: engine:removePico was given null instead of a pico_id string");

    yield assertInvalidPicoID(getAdminECI , "id404", "NotFoundError: Invalid pico_id: id404");
    yield assertInvalidPicoID(getParent   , "id404", "NotFoundError: Invalid pico_id: id404");
    yield assertInvalidPicoID(listChildren, "id404", "NotFoundError: Invalid pico_id: id404");
    yield assertInvalidPicoID(newPico     , "id404", "NotFoundError: Invalid pico_id: id404");
    yield assertInvalidPicoID(removePico  , "id404", "NotFoundError: Invalid pico_id: id404");

    yield testErr(
        t,
        removePico({}, ["id0"]),
        "should have thrown b/c you can't remove a pico with children",
        "Error: Cannot remove pico \"id0\" because it has 2 children"
    );
});

testPE("engine:newChannel, engine:listChannels, engine:removeChannel", function * (t, pe){

    var newChannel = function*(ctx, args){
        return yield runAction(pe, ctx, "engine", "newChannel", args);
    };
    var removeChannel = function*(ctx, args){
        return yield runAction(pe, ctx, "engine", "removeChannel", args);
    };
    var listChannels = yield pe.modules.get({}, "engine", "listChannels");

    var mkChan = function(pico_id, eci, name, type){
        return {
            pico_id: pico_id,
            id: eci,
            name: name,
            type: type,
            sovrin: {
                did: eci,
                verifyKey: "verifyKey_" + eci,
            },
        };
    };

    t.deepEquals(yield listChannels({}, ["id0"]), [
        mkChan("id0", "id1", "admin", "secret"),
    ]);

    t.deepEquals(yield newChannel({}, ["id0", "a", "b"]), mkChan("id0", "id2", "a", "b"));
    t.deepEquals(yield listChannels({}, ["id0"]), [
        mkChan("id0", "id1", "admin", "secret"),
        mkChan("id0", "id2", "a", "b"),
    ]);

    yield testErr(
        t,
        newChannel({}, ["id1"]),
        "should throw b/c no name is given",
        "Error: engine:newChannel needs a name string"
    );
    yield testErr(
        t,
        newChannel({}, ["id1", "id1"]),
        "should throw b/c no type is given",
        "Error: engine:newChannel needs a type string"
    );

    yield testErr(
        t,
        removeChannel({}, ["id1"]),
        "should throw b/c removeChannel shouldn't remove the admin channel",
        "Error: Cannot delete the pico's admin channel"
    );
    yield testErr(
        t,
        removeChannel({}, []),
        "should throw b/c no eci is given",
        "Error: engine:removeChannel needs an eci string"
    );
    yield testErr(
        t,
        removeChannel({}, [/id1/]),
        "should throw b/c wrong eci type",
        "TypeError: engine:removeChannel was given re#id1# instead of an eci string"
    );
    yield testErr(
        t,
        removeChannel({}, ["eci404"]),
        "should throw b/c not found",
        "NotFoundError: Key not found in database [channel,eci404]"
    );

    t.equals(yield removeChannel({}, ["id2"]), void 0);
    t.deepEquals(yield listChannels({}, ["id0"]), [
        mkChan("id0", "id1", "admin", "secret"),
    ]);

    //fallback on ctx.pico_id
    t.deepEquals(yield listChannels({pico_id: "id0"}, []), [
        mkChan("id0", "id1", "admin", "secret"),
    ]);
    t.deepEquals(yield newChannel({pico_id: "id0"}, {"name": "a", "type": "b"}), mkChan("id0", "id3", "a", "b"));

    //report error on invalid pico_id
    var assertInvalidPicoID = function * (genfn, id, expected){
        yield testErr(
            t,
            genfn({pico_id: id}, {"name": "a", "type": "b"}),
            void 0,
            expected
        );
    };

    yield assertInvalidPicoID(newChannel  , void 0, "TypeError: engine:newChannel was given null instead of a pico_id string");
    yield assertInvalidPicoID(listChannels, void 0, "TypeError: engine:listChannels was given null instead of a pico_id string");

    yield assertInvalidPicoID(newChannel  , "id404", "NotFoundError: Invalid pico_id: id404");
    yield assertInvalidPicoID(listChannels, "id404", "NotFoundError: Invalid pico_id: id404");

});


testPE("engine:installRuleset, engine:listInstalledRIDs, engine:uninstallRuleset", function * (t, pe){

    var installRS = function*(ctx, args){
        return yield runAction(pe, ctx, "engine", "installRuleset", args);
    };
    var uninstallRID = function*(ctx, args){
        return yield runAction(pe, ctx, "engine", "uninstallRuleset", args);
    };
    var listRIDs = yield pe.modules.get({}, "engine", "listInstalledRIDs");

    t.deepEquals(yield listRIDs({pico_id: "id0"}, []), [
        "io.picolabs.engine",
    ]);

    t.equals(yield installRS({}, ["id0", "io.picolabs.hello_world"]), "io.picolabs.hello_world");
    yield testErr(
        t,
        installRS({}, [NaN]),
        "should throw b/c no rid or url is given",
        "Error: engine:installRuleset needs either a rid string or array, or a url string"
    );
    yield testErr(
        t,
        installRS({}, ["id0", NaN, 0]),
        "should throw b/c wrong rid type",
        "TypeError: engine:installRuleset was given null instead of a rid string or array"
    );
    yield testErr(
        t,
        installRS({}, ["id0", [[]]]),
        "should throw b/c rid array has a non-string",
        "TypeError: engine:installRuleset was given a rid array containing a non-string ([Array])"
    );
    yield testErr(
        t,
        installRS({"pico_id": "id0"}, {"url": {}}),
        "should throw b/c wrong url type",
        "TypeError: engine:installRuleset was given [Map] instead of a url string"
    );
    t.deepEquals(yield listRIDs({pico_id: "id0"}, []), [
        "io.picolabs.engine",
        "io.picolabs.hello_world",
    ]);

    t.equals(yield uninstallRID({}, ["id0", "io.picolabs.engine"]), void 0);
    yield testErr(
        t,
        uninstallRID({}, []),
        "should throw b/c no rid is given",
        "Error: engine:uninstallRuleset needs a rid string or array"
    );
    yield testErr(
        t,
        uninstallRID({}, ["id0", void 0]),
        "should throw b/c wrong rid type",
        "TypeError: engine:uninstallRuleset was given null instead of a rid string or array"
    );
    yield testErr(
        t,
        uninstallRID({}, ["id0", ["null", null]]),
        "should throw b/c rid array has a non-string",
        "TypeError: engine:uninstallRuleset was given a rid array containing a non-string (null)"
    );
    t.deepEquals(yield listRIDs({pico_id: "id0"}, []), [
        "io.picolabs.hello_world",
    ]);

    //fallback on ctx.pico_id
    t.equals(yield uninstallRID({pico_id: "id0"}, {rid: "io.picolabs.hello_world"}), void 0);
    strictDeepEquals(t, yield listRIDs({pico_id: "id0"}, []), []);
    t.equals(yield installRS({pico_id: "id0"}, {rid: "io.picolabs.hello_world"}), "io.picolabs.hello_world");

    //report error on invalid pico_id
    var assertInvalidPicoID = function * (genfn, id, expected){
        yield testErr(
            t,
            genfn({pico_id: id}, {rid: "io.picolabs.hello_world"}),
            void 0,
            expected
        );
    };

    yield assertInvalidPicoID(listRIDs    , void 0, "TypeError: engine:listInstalledRIDs was given null instead of a pico_id string");

    yield assertInvalidPicoID(installRS   , "id404", "NotFoundError: Invalid pico_id: id404");
    yield assertInvalidPicoID(uninstallRID, "id404", "NotFoundError: Invalid pico_id: id404");
    yield assertInvalidPicoID(listRIDs    , "id404", "NotFoundError: Invalid pico_id: id404");

});
<commit_msg>Make engine.test.js mock assertPicoID like DB.js<commit_after>var _ = require("lodash");
var test = require("tape");
var cocb = require("co-callback");
var ktypes = require("krl-stdlib/types");
var strictDeepEquals = require("krl-stdlib/strictEquals").strictDeepEquals;
var kengine = require("./engine");
var mkTestPicoEngine = require("../mkTestPicoEngine");

//wrap stubbed functions in this to simulate async
var tick = function(fn){
    return function(){
        var args = _.toArray(arguments);
        process.nextTick(function(){
            fn.apply(null, args);
        });
    };
};

var runAction = function*(pe, ctx, domain, id, args){
    var act = yield pe.modules.get(ctx, domain, id);
    return _.head(yield act(ctx, args));
};


var testPE = function(test_name, genfn){
    test(test_name, function(t){
        mkTestPicoEngine({
            rootRIDs: ["io.picolabs.engine"],
        }, function(err, pe){
            if(err) return t.end(err);

            cocb.run(function*(){
                yield genfn(t, pe);
            }, t.end);
        });
    });
};


var testErr = function*(t, genfn, failMsg, errMsg){
    try{
        yield genfn;
        t.fail(failMsg);
    }catch(err){
        t.equals(err + "", errMsg);
    }
};

var assertPicoID = function(id, callback){
    if( ! ktypes.isString(id)){
        return callback(new TypeError("Invalid pico_id: " + ktypes.toString(id)));
    }
    callback(null, id);
};


test("engine:getPicoIDByECI", function(t){
    cocb.run(function*(){
        var engine = kengine({
            db: {
                getPicoIDByECI: tick(function(eci, callback){
                    if(eci === "foo"){
                        return callback(null, "bar");
                    }else if(eci === "baz"){
                        return callback(null, "qux");
                    }
                    callback("NOT FOUND:" + eci);
                })
            }
        });
        var get = function*(eci){
            var eciGiven = eci !== void 0;
            return yield engine.def.getPicoIDByECI(
                {},
                eciGiven ? {eci: eci,} : {}
            );
        };

        t.equals(yield get("foo"), "bar");
        t.equals(yield get("baz"), "qux");

        yield testErr(
            t,
            get(),
            "should throw b/c no eci is given",
            "Error: engine:getPicoIDByECI needs an eci string"
        );
        yield testErr(
            t,
            get(null),
            "should throw b/c wrong eci type",
            "TypeError: engine:getPicoIDByECI was given null instead of an eci string"
        );
        yield testErr(
            t,
            get("quux"),
            "should throw b/c not found",
            "NOT FOUND:quux"
        );

    }, t.end);
});


test("engine:registerRuleset", function(t){
    cocb.run(function*(){

        var engine = kengine({
            registerRulesetURL: tick(function(url, callback){
                callback(null, {
                    rid: "rid for: " + url
                });
            })
        });

        t.equals((yield engine.def.registerRuleset({}, {
            url: "http://foo.bar/qux.krl",
        }))[0], "rid for: http://foo.bar/qux.krl");

        t.equals((yield engine.def.registerRuleset({}, {
            url: "qux.krl",
            base: "https://foo.bar/baz/",
        }))[0], "rid for: https://foo.bar/baz/qux.krl");

        yield testErr(
            t,
            engine.def.registerRuleset({}, []),
            "should throw b/c no url is given",
            "Error: engine:registerRuleset needs a url string"
        );

        yield testErr(
            t,
            engine.def.registerRuleset({}, [_.noop]),
            "should throw b/c wrong url type",
            "TypeError: engine:registerRuleset was given [Function] instead of a url string"
        );

    }, t.end);
});

test("engine:installRuleset", function(t){
    cocb.run(function*(){
        var engine = kengine({
            installRuleset: tick(function(pico_id, rid, callback){
                callback();
            }),
            registerRulesetURL: tick(function(url, callback){
                callback(null, {
                    rid: "REG:" + /\/([^/]*)\.krl$/.exec(url)[1]
                });
            }),
            db: {
                assertPicoID: assertPicoID,
                findRulesetsByURL: tick(function(url, callback){
                    if(url === "http://foo.bar/baz/qux.krl"){
                        return callback(null, [{rid: "found"}]);
                    }else if(url === "file:///too/many.krl"){
                        return callback(null, [{rid: "a"}, {rid: "b"}, {rid: "c"}]);
                    }
                    callback(null, []);
                }),
            }
        });

        var inst = function*(id, rid, url, base){
            var args = {};
            if(id !== void 0){
                args.pico_id = id;
            }
            if(rid !== void 0){
                args.rid = rid;
            }
            if(url !== void 0){
                args.url = url;
            }
            if(base !== void 0){
                args.base = base;
            }
            return (yield engine.def.installRuleset({}, args))[0];
        };

        t.equals(yield inst("pico0", "foo.bar"), "foo.bar");
        t.deepEquals(yield inst("pico0", ["foo.bar", "foo.qux"]), ["foo.bar", "foo.qux"]);
        strictDeepEquals(t, yield inst("pico0", []), []);
        t.deepEquals(yield inst("pico0", void 0, "file:///foo/bar.krl"), "REG:bar");
        t.deepEquals(yield inst("pico0", void 0, "qux.krl", "http://foo.bar/baz/"), "found");

        yield testErr(
            t,
            inst("pico0", void 0, "file:///too/many.krl"),
            "should throw b/c too many matched",
            "Error: More than one rid found for the given url: a , b , c"
        );

    }, t.end);
});

test("engine:uninstallRuleset", function(t){
    cocb.run(function*(){

        var uninstalled = {};
        var order = 0;

        var engine = kengine({
            uninstallRuleset: tick(function(id, rid, callback){
                if(id !== "pico0"){
                    return callback(new Error("invalid pico_id"));
                }
                if(!_.isString(rid)){
                    return callback(new Error("invalid rid"));
                }
                _.set(uninstalled, [id, rid], order++);
                callback();
            }),
            db: {
                assertPicoID: assertPicoID,
            }
        });

        t.equals((yield engine.def.uninstallRuleset({}, {
            pico_id: "pico0",
            rid: "foo.bar",
        }))[0], void 0);

        t.equals((yield engine.def.uninstallRuleset({}, {
            pico_id: "pico0",
            rid: ["baz", "qux"],
        }))[0], void 0);

        t.deepEquals(uninstalled, {
            pico0: {
                "foo.bar": 0,
                "baz": 1,
                "qux": 2,
            }
        });

    }, t.end);
});

test("engine:unregisterRuleset", function(t){
    cocb.run(function*(){
        var log = [];
        var engine = kengine({
            unregisterRuleset: tick(function(rid, callback){
                if(!_.isString(rid)){
                    return callback("invalid rid");
                }
                log.push(rid);
                callback();
            }),
        });

        t.equals((yield engine.def.unregisterRuleset({}, {
            rid: "foo.bar",
        }))[0], void 0);

        t.equals((yield engine.def.unregisterRuleset({}, {
            rid: ["baz", "qux"],
        }))[0], void 0);

        yield testErr(
            t,
            engine.def.unregisterRuleset({}, []),
            void 0,
            "Error: engine:unregisterRuleset needs a rid string or array"
        );

        yield testErr(
            t,
            engine.def.unregisterRuleset({}, {rid: {},}),
            void 0,
            "TypeError: engine:unregisterRuleset was given [Map] instead of a rid string or array"
        );

        yield testErr(
            t,
            engine.def.unregisterRuleset({}, {
                rid: ["baz", 2, "qux"],
            }),
            void 0,
            "TypeError: engine:unregisterRuleset was given a rid array containing a non-string (2)"
        );

        t.deepEquals(log, [
            "foo.bar",
            "baz",
            "qux",
        ]);

    }, t.end);
});

testPE("engine:describeRuleset", function * (t, pe){
    var ctx = {};
    var descRID = yield pe.modules.get(ctx, "engine", "describeRuleset");

    var desc = yield descRID(ctx, {rid: "io.picolabs.hello_world"});

    var isIsoString = function(str){
        return str === (new Date(str)).toISOString();
    };

    t.deepEquals(_.keys(desc), [
        "rid",
        "src",
        "hash",
        "url",
        "timestamp_stored",
        "timestamp_enable",
        "meta",
    ]);
    t.equals(desc.rid, "io.picolabs.hello_world");
    t.ok(_.isString(desc.src));
    t.ok(_.isString(desc.hash));
    t.ok(_.isString(desc.url));
    t.ok(isIsoString(desc.timestamp_stored));
    t.ok(isIsoString(desc.timestamp_enable));
    t.deepEquals(desc.meta, {
        name: "<NAME>",
        description: "\nA first ruleset for the Quickstart\n        ",
        author: "<NAME>",
    });

    yield testErr(
        t,
        descRID(ctx, []),
        "should throw b/c no rid is given",
        "Error: engine:describeRuleset needs a rid string"
    );
    yield testErr(
        t,
        descRID(ctx, [[]]),
        "should throw b/c wrong rid type",
        "TypeError: engine:describeRuleset was given [Array] instead of a rid string"
    );
    try{
        yield descRID(ctx, {rid: "not.found"});
        t.fail("should fail b/c not found");
    }catch(err){
        t.ok(err && err.notFound);
    }
});

testPE("engine:listInstalledRIDs", function * (t, pe){
    var ctx = {};
    var listRIDs = yield pe.modules.get(ctx, "engine", "listInstalledRIDs");

    var rids = yield listRIDs(ctx, {pico_id: "id0"});

    t.deepEquals(rids, [
        "io.picolabs.engine",
    ]);
});

//TODO testPE("engine:listAllEnabledRIDs...

testPE("engine:newPico", function * (t, pe){
    var action = function*(ctx, name, args){
        return yield runAction(pe, ctx, "engine", name, args);
    };

    var pico2 = yield action({}, "newPico", {
        parent_id: "id0",
    });
    t.deepEquals(pico2, {
        id: "id2",
        parent_id: "id0",
        admin_eci: "id3",
    });

    //default to ctx.pico_id
    var pico3 = yield action({
        pico_id: "id2",//called by pico2
    }, "newPico", {});
    t.deepEquals(pico3, {
        id: "id4",
        parent_id: "id2",
        admin_eci: "id5",
    });
});


testPE("engine:getParent, engine:getAdminECI, engine:listChildren, engine:removePico", function * (t, pe){

    var newPico = function*(ctx, args){
        return yield runAction(pe, ctx, "engine", "newPico", args);
    };
    var removePico = function*(ctx, args){
        return yield runAction(pe, ctx, "engine", "removePico", args);
    };

    var getParent = yield pe.modules.get({}, "engine", "getParent");
    var getAdminECI = yield pe.modules.get({}, "engine", "getAdminECI");
    var listChildren = yield pe.modules.get({}, "engine", "listChildren");

    yield newPico({pico_id: "id0"}, []);// id2
    yield newPico({}, ["id0"]);// id4
    yield newPico({pico_id: "id2"}, []);// id6

    t.equals(yield getParent({}, ["id0"]), null);
    t.equals(yield getParent({}, ["id2"]), "id0");
    t.equals(yield getParent({}, ["id4"]), "id0");
    t.equals(yield getParent({}, ["id6"]), "id2");

    t.equals(yield getAdminECI({}, ["id0"]), "id1");
    t.equals(yield getAdminECI({}, ["id2"]), "id3");
    t.equals(yield getAdminECI({}, ["id4"]), "id5");
    t.equals(yield getAdminECI({}, ["id6"]), "id7");

    t.deepEquals(yield listChildren({}, ["id0"]), ["id2", "id4"]);
    t.deepEquals(yield listChildren({}, ["id2"]), ["id6"]);
    strictDeepEquals(t, yield listChildren({}, ["id4"]), []);
    strictDeepEquals(t, yield listChildren({}, ["id6"]), []);

    //fallback on ctx.pico_id
    t.equals(yield getParent({pico_id: "id6"}, []), "id2");
    t.equals(yield getAdminECI({pico_id: "id6"}, []), "id7");
    t.deepEquals(yield listChildren({pico_id: "id2"}, []), ["id6"]);
    t.equals(yield removePico({pico_id: "id6"}, []), void 0);
    strictDeepEquals(t, yield listChildren({}, ["id2"]), []);

    //report error on invalid pico_id
    var assertInvalidPicoID = function * (genfn, id, expected){
        yield testErr(
            t,
            genfn({pico_id: id}, []),
            void 0,
            expected
        );
    };

    yield assertInvalidPicoID(getParent   , void 0, "TypeError: engine:getParent was given null instead of a pico_id string");
    yield assertInvalidPicoID(getAdminECI , void 0, "TypeError: engine:getAdminECI was given null instead of a pico_id string");
    yield assertInvalidPicoID(listChildren, void 0, "TypeError: engine:listChildren was given null instead of a pico_id string");
    yield assertInvalidPicoID(newPico     , void 0, "TypeError: engine:newPico was given null instead of a parent_id string");
    yield assertInvalidPicoID(removePico  , void 0, "TypeError: engine:removePico was given null instead of a pico_id string");

    yield assertInvalidPicoID(getAdminECI , "id404", "NotFoundError: Invalid pico_id: id404");
    yield assertInvalidPicoID(getParent   , "id404", "NotFoundError: Invalid pico_id: id404");
    yield assertInvalidPicoID(listChildren, "id404", "NotFoundError: Invalid pico_id: id404");
    yield assertInvalidPicoID(newPico     , "id404", "NotFoundError: Invalid pico_id: id404");
    yield assertInvalidPicoID(removePico  , "id404", "NotFoundError: Invalid pico_id: id404");

    yield testErr(
        t,
        removePico({}, ["id0"]),
        "should have thrown b/c you can't remove a pico with children",
        "Error: Cannot remove pico \"id0\" because it has 2 children"
    );
});

testPE("engine:newChannel, engine:listChannels, engine:removeChannel", function * (t, pe){

    var newChannel = function*(ctx, args){
        return yield runAction(pe, ctx, "engine", "newChannel", args);
    };
    var removeChannel = function*(ctx, args){
        return yield runAction(pe, ctx, "engine", "removeChannel", args);
    };
    var listChannels = yield pe.modules.get({}, "engine", "listChannels");

    var mkChan = function(pico_id, eci, name, type){
        return {
            pico_id: pico_id,
            id: eci,
            name: name,
            type: type,
            sovrin: {
                did: eci,
                verifyKey: "verifyKey_" + eci,
            },
        };
    };

    t.deepEquals(yield listChannels({}, ["id0"]), [
        mkChan("id0", "id1", "admin", "secret"),
    ]);

    t.deepEquals(yield newChannel({}, ["id0", "a", "b"]), mkChan("id0", "id2", "a", "b"));
    t.deepEquals(yield listChannels({}, ["id0"]), [
        mkChan("id0", "id1", "admin", "secret"),
        mkChan("id0", "id2", "a", "b"),
    ]);

    yield testErr(
        t,
        newChannel({}, ["id1"]),
        "should throw b/c no name is given",
        "Error: engine:newChannel needs a name string"
    );
    yield testErr(
        t,
        newChannel({}, ["id1", "id1"]),
        "should throw b/c no type is given",
        "Error: engine:newChannel needs a type string"
    );

    yield testErr(
        t,
        removeChannel({}, ["id1"]),
        "should throw b/c removeChannel shouldn't remove the admin channel",
        "Error: Cannot delete the pico's admin channel"
    );
    yield testErr(
        t,
        removeChannel({}, []),
        "should throw b/c no eci is given",
        "Error: engine:removeChannel needs an eci string"
    );
    yield testErr(
        t,
        removeChannel({}, [/id1/]),
        "should throw b/c wrong eci type",
        "TypeError: engine:removeChannel was given re#id1# instead of an eci string"
    );
    yield testErr(
        t,
        removeChannel({}, ["eci404"]),
        "should throw b/c not found",
        "NotFoundError: Key not found in database [channel,eci404]"
    );

    t.equals(yield removeChannel({}, ["id2"]), void 0);
    t.deepEquals(yield listChannels({}, ["id0"]), [
        mkChan("id0", "id1", "admin", "secret"),
    ]);

    //fallback on ctx.pico_id
    t.deepEquals(yield listChannels({pico_id: "id0"}, []), [
        mkChan("id0", "id1", "admin", "secret"),
    ]);
    t.deepEquals(yield newChannel({pico_id: "id0"}, {"name": "a", "type": "b"}), mkChan("id0", "id3", "a", "b"));

    //report error on invalid pico_id
    var assertInvalidPicoID = function * (genfn, id, expected){
        yield testErr(
            t,
            genfn({pico_id: id}, {"name": "a", "type": "b"}),
            void 0,
            expected
        );
    };

    yield assertInvalidPicoID(newChannel  , void 0, "TypeError: engine:newChannel was given null instead of a pico_id string");
    yield assertInvalidPicoID(listChannels, void 0, "TypeError: engine:listChannels was given null instead of a pico_id string");

    yield assertInvalidPicoID(newChannel  , "id404", "NotFoundError: Invalid pico_id: id404");
    yield assertInvalidPicoID(listChannels, "id404", "NotFoundError: Invalid pico_id: id404");

});


testPE("engine:installRuleset, engine:listInstalledRIDs, engine:uninstallRuleset", function * (t, pe){

    var installRS = function*(ctx, args){
        return yield runAction(pe, ctx, "engine", "installRuleset", args);
    };
    var uninstallRID = function*(ctx, args){
        return yield runAction(pe, ctx, "engine", "uninstallRuleset", args);
    };
    var listRIDs = yield pe.modules.get({}, "engine", "listInstalledRIDs");

    t.deepEquals(yield listRIDs({pico_id: "id0"}, []), [
        "io.picolabs.engine",
    ]);

    t.equals(yield installRS({}, ["id0", "io.picolabs.hello_world"]), "io.picolabs.hello_world");
    yield testErr(
        t,
        installRS({}, [NaN]),
        "should throw b/c no rid or url is given",
        "Error: engine:installRuleset needs either a rid string or array, or a url string"
    );
    yield testErr(
        t,
        installRS({}, ["id0", NaN, 0]),
        "should throw b/c wrong rid type",
        "TypeError: engine:installRuleset was given null instead of a rid string or array"
    );
    yield testErr(
        t,
        installRS({}, ["id0", [[]]]),
        "should throw b/c rid array has a non-string",
        "TypeError: engine:installRuleset was given a rid array containing a non-string ([Array])"
    );
    yield testErr(
        t,
        installRS({"pico_id": "id0"}, {"url": {}}),
        "should throw b/c wrong url type",
        "TypeError: engine:installRuleset was given [Map] instead of a url string"
    );
    t.deepEquals(yield listRIDs({pico_id: "id0"}, []), [
        "io.picolabs.engine",
        "io.picolabs.hello_world",
    ]);

    t.equals(yield uninstallRID({}, ["id0", "io.picolabs.engine"]), void 0);
    yield testErr(
        t,
        uninstallRID({}, []),
        "should throw b/c no rid is given",
        "Error: engine:uninstallRuleset needs a rid string or array"
    );
    yield testErr(
        t,
        uninstallRID({}, ["id0", void 0]),
        "should throw b/c wrong rid type",
        "TypeError: engine:uninstallRuleset was given null instead of a rid string or array"
    );
    yield testErr(
        t,
        uninstallRID({}, ["id0", ["null", null]]),
        "should throw b/c rid array has a non-string",
        "TypeError: engine:uninstallRuleset was given a rid array containing a non-string (null)"
    );
    t.deepEquals(yield listRIDs({pico_id: "id0"}, []), [
        "io.picolabs.hello_world",
    ]);

    //fallback on ctx.pico_id
    t.equals(yield uninstallRID({pico_id: "id0"}, {rid: "io.picolabs.hello_world"}), void 0);
    strictDeepEquals(t, yield listRIDs({pico_id: "id0"}, []), []);
    t.equals(yield installRS({pico_id: "id0"}, {rid: "io.picolabs.hello_world"}), "io.picolabs.hello_world");

    //report error on invalid pico_id
    var assertInvalidPicoID = function * (genfn, id, expected){
        yield testErr(
            t,
            genfn({pico_id: id}, {rid: "io.picolabs.hello_world"}),
            void 0,
            expected
        );
    };

    yield assertInvalidPicoID(listRIDs    , void 0, "TypeError: engine:listInstalledRIDs was given null instead of a pico_id string");

    yield assertInvalidPicoID(installRS   , "id404", "NotFoundError: Invalid pico_id: id404");
    yield assertInvalidPicoID(uninstallRID, "id404", "NotFoundError: Invalid pico_id: id404");
    yield assertInvalidPicoID(listRIDs    , "id404", "NotFoundError: Invalid pico_id: id404");

});

{'fraction_non_alphanumeric': 0.16004324843825085, 'fraction_numerical': 0.010331571359923113, 'mean_word_length': 2.1480977233189624, 'pattern_counts': {'":': 18, '<': 7, '<?xml version=': 0, '>': 7, 'https://': 4, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '930762', 'n_tokens_mistral': 13648, 'n_tokens_neox': 13043, 'n_words': 3199}