fix(assets): expose full inventory for scoped users and stabilize previews
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/push/woodpecker Pipeline was successful
This commit is contained in:
@@ -15,8 +15,8 @@ const plan = (overrides: Partial<QueryPlan> = {}): QueryPlan => ({
|
||||
const part = (id: string, title: string, queryPlan: QueryPlan, display = "table") => ({ id, title, question: title, plan: queryPlan, display });
|
||||
let serial = 0;
|
||||
|
||||
function fixture(t: TestContext) {
|
||||
const user: AuthUser = { userId: `composition-${++serial}`, userName: "组合验收", loginName: "composition", depCode: "D", depName: "合成部门", permissionLevel: "personal", roles: [] };
|
||||
function fixture(t: TestContext, permissionLevel: AuthUser["permissionLevel"] = "personal") {
|
||||
const user: AuthUser = { userId: `composition-${++serial}`, userName: "组合验收", loginName: "composition", depCode: "D", depName: "合成部门", permissionLevel, roles: [] };
|
||||
const vehicles = [
|
||||
{ id: "jx-inventory", type: "4.5T", city: "嘉兴市", status: "Inventory", operationStatus: "3" },
|
||||
{ id: "hz-inventory", type: "18T", city: "杭州市", status: "Inventory", operationStatus: "3" },
|
||||
@@ -24,7 +24,7 @@ function fixture(t: TestContext) {
|
||||
{ id: "jx-operating", type: "18T", city: "嘉兴市", status: "Operating", operationStatus: "1" },
|
||||
{ id: "south-inventory", type: "49T", city: "广州市", province: "广东省", status: "Inventory", operationStatus: "3" },
|
||||
{ id: "other-company", type: "4.5T", city: "嘉兴市", subjectOrg: "合成公司B", status: "Inventory", operationStatus: "3" },
|
||||
{ id: "other-owner", type: "4.5T", city: "嘉兴市", managerId: "another-user", status: "Inventory", operationStatus: "3" },
|
||||
{ id: "other-owner", type: "4.5T", city: "嘉兴市", managerId: "another-user", departmentName: "其他部门", status: "Inventory", operationStatus: "3" },
|
||||
].map((v) => ({ province: "浙江省", model: "合成车型", subjectOrg: company, departmentName: "合成部门", managerId: user.userId, plateNumber: `合成-${v.id}`, vin: `SYNTHETIC-${v.id}`, ...v }) as Vehicle);
|
||||
const getVehicles = t.mock.method(vehicleRepository, "getVehicles", async () => vehicles);
|
||||
let flowFailure = false;
|
||||
@@ -74,7 +74,7 @@ function fixture(t: TestContext) {
|
||||
assert.ok(result, "A composed response must produce a result even if one section fails");
|
||||
return result;
|
||||
}
|
||||
return { ask, proposals, requests, presentationRequests, gaps, getVehicles, getFlow, vehicles, failFlow: () => { flowFailure = true; } };
|
||||
return { app, ask, proposals, requests, presentationRequests, gaps, getVehicles, getFlow, vehicles, failFlow: () => { flowFailure = true; } };
|
||||
}
|
||||
const based = (result: AssistantResult) => ({ sessionId: result.sessionId, baseResultId: result.resultId });
|
||||
const ids = (result: AssistantResult) => result.rows.map((row) => row.id).sort();
|
||||
@@ -93,7 +93,7 @@ test("one model decision executes four independent analyses in the same authoriz
|
||||
const result = await ask({ message: "分别展示华东资产概况、库存分布、待交车清单和运营车型统计。" });
|
||||
assert.equal(result.sections?.length, 4);
|
||||
assert.ok(result.resultId);
|
||||
const counts = [4, 3, 1, 1];
|
||||
const counts = [5, 4, 1, 1];
|
||||
const childIds: string[] = [];
|
||||
for (const [index, section] of result.sections!.entries()) {
|
||||
assert.equal(section.id, sections[index].id);
|
||||
@@ -106,7 +106,7 @@ test("one model decision executes four independent analyses in the same authoriz
|
||||
assert.equal(section.result.sessionId, result.sessionId);
|
||||
assert.ok(section.result.resultId);
|
||||
childIds.push(section.result.resultId!);
|
||||
assert.ok(ids(section.result).every((id) => !["other-company", "other-owner", "south-inventory"].includes(id)));
|
||||
assert.ok(ids(section.result).every((id) => !["other-company", "south-inventory"].includes(id)));
|
||||
}
|
||||
assert.equal(new Set([result.resultId, ...childIds]).size, 5, "Root and each analysis need distinct result identities");
|
||||
assert.equal(result.outcome, "complete");
|
||||
@@ -125,7 +125,7 @@ for (const failedFirst of [true, false]) {
|
||||
assert.equal(result.sections?.length, 2);
|
||||
const asset = result.sections!.find((section) => section.id === "inventory")!;
|
||||
const failed = result.sections!.find((section) => section.id === "flow")!;
|
||||
assert.equal(asset.result?.rows.length, 3);
|
||||
assert.equal(asset.result?.rows.length, 4);
|
||||
assert.ok(asset.result?.resultId);
|
||||
assert.ok(failed.error);
|
||||
assert.equal(failed.result, undefined);
|
||||
@@ -146,7 +146,7 @@ test("drilling and following up a child preserves that child's category and filt
|
||||
assert.equal(drill.sections, undefined, "A local block action must return a normal result for in-place replacement");
|
||||
assert.equal(drill.plan.category, "inventory");
|
||||
assert.deepEqual(drill.plan.filters, { region: ["华东"], city: ["嘉兴市"] });
|
||||
assert.deepEqual(ids(drill), ["jx-inventory", "jx-pending"]);
|
||||
assert.deepEqual(ids(drill), ["jx-inventory", "jx-pending", "other-owner"]);
|
||||
assert.equal(requests.length, 1, "Click-based drilling should not trigger another planning request");
|
||||
assert.equal(presentationRequests.length, 2, "The original result and clicked result each get one display generation pass");
|
||||
proposals.push({ kind: "query", plan: plan({ category: "pending", view: "list", filters: { region: ["华东"], city: ["嘉兴市"] } }) });
|
||||
@@ -169,7 +169,7 @@ test("the parent conversation includes all successful analyses for cross-section
|
||||
const analyses = context.analyses || context.resultContext?.analyses;
|
||||
assert.ok(Array.isArray(analyses), "The model needs every analysis, not only the root's first plan");
|
||||
assert.equal(analyses.length, 2);
|
||||
assert.ok(analyses.some((analysis: { plan: QueryPlan; metrics: { value: number }[] }) => analysis.plan.category === "inventory" && analysis.metrics[0].value === 3));
|
||||
assert.ok(analyses.some((analysis: { plan: QueryPlan; metrics: { value: number }[] }) => analysis.plan.category === "inventory" && analysis.metrics[0].value === 4));
|
||||
assert.ok(analyses.some((analysis: { id: string; plan: QueryPlan; metrics: { value: number }[] }) => analysis.id === pending.resultId && analysis.plan.category === "pending" && analysis.metrics[0].value === 1));
|
||||
assert.equal(next.plan.category, "pending");
|
||||
assert.deepEqual(ids(next), ["jx-pending"]);
|
||||
@@ -185,7 +185,7 @@ test("an unknown display or invalid child plan cannot discard the healthy siblin
|
||||
] });
|
||||
const result = await ask({ message: "库存分布、待交清单和另外一项查询分别显示。" });
|
||||
const healthy = result.sections?.find((section) => section.id === "inventory")?.result || result;
|
||||
assert.equal(healthy.rows.length, 3);
|
||||
assert.equal(healthy.rows.length, 4);
|
||||
assert.equal(healthy.plan.category, "inventory");
|
||||
const display = result.sections?.find((section) => section.id === "unknown-display");
|
||||
if (display) {
|
||||
@@ -207,8 +207,8 @@ test("a simple query keeps the legacy result shape and uses one planning call",
|
||||
proposals.push({ kind: "query", plan: plan() });
|
||||
const result = await ask({ message: "华东库存按城市统计" });
|
||||
assert.equal(result.sections, undefined);
|
||||
assert.equal(result.rows.length, 3);
|
||||
assert.equal(result.metrics[0].value, 3);
|
||||
assert.equal(result.rows.length, 4);
|
||||
assert.equal(result.metrics[0].value, 4);
|
||||
assert.ok(result.resultId);
|
||||
assert.equal(requests.length, 1);
|
||||
assert.equal(presentationRequests.length, 1);
|
||||
@@ -223,11 +223,11 @@ test("inventory subset percentages use the full inventory denominator even when
|
||||
proposals.push({ kind: "query", plan: stock, queries: [part("stock-share", "华东库存分布与子集占比", stock, "chart"), part("pending", "华东待交车清单", plan({ category: "pending", view: "list" }), "list")] });
|
||||
const result = await ask({ message: "华东库存最多的城市和待交车清单分别展示,同时给待交车与异动占库存比例。" });
|
||||
const stockResult = result.sections!.find((section) => section.id === "stock-share")!.result!;
|
||||
assert.equal(stockResult.rows.length, 3);
|
||||
assert.equal(stockResult.rows.length, 4);
|
||||
assert.equal(stockResult.groups.length, 1);
|
||||
assert.equal(stockResult.groups[0].count, 2);
|
||||
assert.equal(stockResult.metrics.find((metric) => metric.label === "待交车占库存")?.value, 33.3);
|
||||
assert.equal(stockResult.metrics.find((metric) => metric.label === "异动占库存")?.value, 33.3);
|
||||
assert.equal(stockResult.groups[0].count, 3);
|
||||
assert.equal(stockResult.metrics.find((metric) => metric.label === "待交车占库存")?.value, 25);
|
||||
assert.equal(stockResult.metrics.find((metric) => metric.label === "异动占库存")?.value, 25);
|
||||
assert.equal(stockResult.metrics.find((metric) => metric.label === "待交车占库存")?.unit, "%");
|
||||
});
|
||||
|
||||
@@ -249,7 +249,7 @@ test("explicitly changing the parent region for composed analyses does not resto
|
||||
const { ask, proposals } = fixture(t);
|
||||
proposals.push({ kind: "query", plan: plan({ filters: { region: ["华东"], city: ["嘉兴市"] } }) });
|
||||
const previous = await ask({ message: "只看华东嘉兴库存" });
|
||||
assert.equal(previous.rows.length, 2);
|
||||
assert.equal(previous.rows.length, 3);
|
||||
const southernInventory = plan({ filters: { region: ["华南"] } });
|
||||
const southernOperating = plan({ category: "operating", filters: { region: ["华南"] } });
|
||||
proposals.push({ kind: "query", plan: southernInventory, queries: [
|
||||
@@ -262,3 +262,28 @@ test("explicitly changing the parent region for composed analyses does not resto
|
||||
assert.deepEqual(ids(inventoryResult), ["south-inventory"]);
|
||||
assert.ok(changed.sections!.every((section) => !section.result?.plan.filters.city));
|
||||
});
|
||||
|
||||
for (const permissionLevel of ["personal", "department"] as const) {
|
||||
test(`${permissionLevel} can analyze public stock but flow, comparisons, actions and exports retain original scope`, async (t) => {
|
||||
const { app, ask, proposals } = fixture(t, permissionLevel);
|
||||
const compared = { ...flow(), plan: { ...flow().plan, comparePrevious: true } };
|
||||
proposals.push({ kind: "query", plan: plan(), queries: [inventory(), compared] });
|
||||
const result = await ask({ message: "库存和交退换趋势分别展示,并对比上期" });
|
||||
const stock = result.sections!.find(section => section.id === "inventory")!.result!;
|
||||
const business = result.sections!.find(section => section.id === "flow")!.result!;
|
||||
assert.ok(ids(stock).includes("other-owner"), "Other departments' stock is public");
|
||||
assert.deepEqual(ids(business), ["event-valid"], "Public stock does not expose other owners' historical events");
|
||||
assert.equal(business.comparison![0].current, 1);
|
||||
assert.equal(business.comparison![0].previous, 1);
|
||||
const day = await ask({ ...based(business), action: { type: "day", date: business.plan.start! } });
|
||||
assert.deepEqual(ids(day), ["event-valid"]);
|
||||
const exported = await app.request("/ai/export", {
|
||||
method: "POST", headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ sessionId: business.sessionId, resultId: business.resultId, subject: company }),
|
||||
});
|
||||
assert.equal(exported.status, 200);
|
||||
const csv = await exported.text();
|
||||
assert.match(csv, /合成-jx-inventory/);
|
||||
assert.doesNotMatch(csv, /合成-other-owner|合成-other-company/);
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user