feat: prepare bids by official lot selection

This commit is contained in:
2026-09-06 08:52:07 +03:00
parent 311344306b
commit 0d332afdb1
6 changed files with 162 additions and 13 deletions
@@ -0,0 +1,83 @@
- checkbox "Yalnız seçtiğim resmi kısımlara teklif hazırlıyorum" [checked]
- text: Yalnız seçtiğim resmi kısımlara teklif hazırlıyorum
- text: Kısmi teklif izni — resmi doküman ve madde
- textbox "Kısmi teklif izni — resmi doküman ve madde": Sentetik QA örneği madde 19
- paragraph: Resmi kısım numaralarını cetvelden doğrulayın. Aynı kısımdaki tüm kalemler birlikte seçilir; seçilmeyenler toplam ve çıktı dışında kalır.
- checkbox "Kısım 1" [checked]
- text: Kısım 1
- generic: Sentetik test kalemi
- generic: Resmi kısım
- textbox "Sentetik test kalemi resmi kısım": "1"
- heading "Kalem fiyatlandırma" [level=5]
- paragraph: Ondalık fiyatı 125,50 veya 125.50 biçiminde yazın; binlik ayırıcı kullanmayın.
- table:
- rowgroup:
- row "Kalem Miktar Birim fiyat Tutar":
- columnheader "Kalem"
- columnheader "Miktar"
- columnheader "Birim fiyat"
- columnheader "Tutar"
- rowgroup:
- row "Sentetik test kalemi 2 adet 125,50 251,00":
- cell "Sentetik test kalemi"
- cell "2 adet"
- cell "125,50":
- textbox "0": 125,50
- cell "251,00"
- region "Belge ve ürün uygunluğu":
- heading "Belge ve ürün uygunluğu" [level=5]
- paragraph: Her kontrol için belge adı/sürümü ve kaynak maddeyi yazın. ÜTS, numune ve servis koşulları her ihalede zorunlu değildir; uygulanmıyorsa gerekçesini belirtin. Bu kayıtlar kullanıcı kontrolüdür; resmi uygunluk onayı değildir.
- group "Sentetik yetki belgesi (zorunlu)":
- generic: Sentetik yetki belgesi (zorunlu)
- paragraph: Yerel QA dokümanı madde 7
- button "Elle eklenen gereksinimi kaldır"
- combobox "Kontrol durumu":
- option "Kontrol gerekli"
- option "Doğrulandı" [selected]
- text: Belge adı/sürümü, geçerlilik kontrolü ve kaynak madde
- textbox "Belge adı/sürümü, geçerlilik kontrolü ve kaynak madde": QA dosyası v1 — yalnız test, madde 7
- paragraph: Resmi dokümandan eksik belge gereksinimi ekle
- text: Belge adı
- textbox "Belge adı"
- text: Kaynak doküman ve madde
- textbox "Kaynak doküman ve madde"
- button "Belge gereksinimi ekle" [disabled]
- generic "Sentetik test kalemi — ürün/hizmet ve yükümlülük kontrolü"
- option "Kontrol gerekli"
- option "Uygun" [selected]
- option "Uygun değil"
- option "Kontrol gerekli"
- option "Doğrulandı"
- option "Uygulanmıyor — gerekçe gerekli" [selected]
- option "Kontrol gerekli"
- option "Doğrulandı"
- option "Uygulanmıyor — gerekçe gerekli" [selected]
- option "Kontrol gerekli"
- option "Doğrulandı"
- option "Uygulanmıyor — gerekçe gerekli" [selected]
- option "Kontrol gerekli"
- option "Doğrulandı"
- option "Uygulanmıyor — gerekçe gerekli" [selected]
- generic: Teklif toplamı (KDV hariç)
- generic: 251,00 TL
- paragraph: "Yazı ile: ikiyüzellibir Türk Lirası"
- heading "Hazırlanacak belgeler" [level=5]
- button "Teklif Mektubu Rakam ve yazı ile bedel Yazdır / PDF" [disabled]:
- generic: Teklif Mektubu
- generic: Rakam ve yazı ile bedel
- text: Yazdır / PDF
- button "Birim Fiyat Teklif Cetveli 1 kalem Yazdır / PDF" [disabled]:
- generic: Birim Fiyat Teklif Cetveli
- generic: 1 kalem
- text: Yazdır / PDF
- button "Evrak Kontrol Listesi 1 belge Yazdır / PDF" [disabled]:
- generic: Evrak Kontrol Listesi
- generic: 1 belge
- text: Yazdır / PDF
- paragraph: Tutar, kaynak, belge ve ürün uygunluğu kontrollerini tamamlayın.
- paragraph: Belgeler taslaktır ve idarenin standart formlarının yerine geçmez. İmzalamadan önce ihale dokümanındaki form metinleriyle karşılaştırın.
- button
- button "Kullanım kılavuzu (? tuşu)":
- region "Notifications (F8)":
- list
+9 -1
View File
@@ -71,9 +71,17 @@ Tarayıcı kanıtı: [Yenileme sonrası beş kart](evidence/qa-kanban-after-relo
- Düzeltme sonrasında tarayıcıda `125,50 × 2 = 251,00 TL` görüldü ve yeniden yüklemede korundu. Bir otomatik test ekran hesabı/PDF HTML tutarlılığı, geçersiz sayı ve aşırı tutarı kapsar. - Düzeltme sonrasında tarayıcıda `125,50 × 2 = 251,00 TL` görüldü ve yeniden yüklemede korundu. Bir otomatik test ekran hesabı/PDF HTML tutarlılığı, geçersiz sayı ve aşırı tutarı kapsar.
- Kanıtlar: [taslak yenilemesi](evidence/qa-draft-after-reload.txt), [virgüllü fiyat](evidence/qa-turkish-price-fixed.txt), [kontrol yenilemesi](evidence/qa-checks-after-reload.txt). - Kanıtlar: [taslak yenilemesi](evidence/qa-draft-after-reload.txt), [virgüllü fiyat](evidence/qa-turkish-price-fixed.txt), [kontrol yenilemesi](evidence/qa-checks-after-reload.txt).
## Kısmi teklif hazırlığı
- Kısmi teklif izninin doküman/madde kaydı, resmi kısım eşleştirmesi ve kısım seçimi eklendi. Bilinmeyen kısım otomatik tahmin edilmez; kaynakta kısım varsa kullanıcı üzerine yazamaz.
- Aynı kısma ait tüm satırlar birlikte seçilir. Yalnız seçili satırlar fiyat, ürün uygunluğu ve cetvel çıktısına girer. Kısım değişince kaynak karşılaştırması yeniden gerekir; önceki fiyatlar kendi satırında korunur.
- Seçim, eşleştirmeler ve izin kaynağı hazırlık taslağına kaydedilir. Eski seçili kısım güncel kaynakta yoksa çıktı engellenir.
- İki test seçilmeyen 1.000 TL cihazın 80 TL'lik seçili cetvele karışmamasını, eksik kaynak/eşleştirme ve kaybolan kısım engellerini doğrular. Tarayıcıda tek sentetik satır için boş seçim engeli, kısım eşleştirmesi ve seçili fiyat gösterimi doğrulandı: [kanıt](evidence/qa-lot-selection.txt).
- Aksaray'ın gerçek 34 kısmının resmi dokümanıyla karşılaştırma ve çok kısımlı tarayıcı kabulü henüz yoktur. Götürü bedel tutarı kullanıcı tarafından seçilen kapsam için ayrıca girilir.
## Doğrulama ## Doğrulama
`npm test`: 31/31 geçti (son yeniden çalıştırma: 6 Eylül 2026). `npm test`: 33/33 geçti (son yeniden çalıştırma: 6 Eylül 2026).
`npm run build`: geçti; mevcut büyük paket/dinamik import uyarıları sürüyor. `npm run build`: geçti; mevcut büyük paket/dinamik import uyarıları sürüyor.
`node --check server/index.js`, `node --check server/user-store.mjs`, `git diff --check`: geçti. `node --check server/index.js`, `node --check server/user-store.mjs`, `git diff --check`: geçti.
+36 -11
View File
@@ -1,6 +1,7 @@
import React, { useState, useMemo } from 'react'; import React, { useState, useMemo } from 'react';
import { bidRowTotal, validateBidRows, roundMoney, parseBidPrice } from '../services/bidValidation'; import { bidRowTotal, validateBidRows, roundMoney, parseBidPrice } from '../services/bidValidation';
import { sourceRows, bidSourceSnapshot, restoreBidPrices } from '../services/bidSource'; import { sourceRows, bidSourceSnapshot, restoreBidPrices } from '../services/bidSource';
import { rowLot, selectBidLots } from '../services/bidLots';
import { PreparationChecks } from './PreparationChecks'; import { PreparationChecks } from './PreparationChecks';
import { preparationBlockers, documentKey } from '../services/preparationChecks'; import { preparationBlockers, documentKey } from '../services/preparationChecks';
import { useApp } from '../context/AppContext'; import { useApp } from '../context/AppContext';
@@ -66,8 +67,11 @@ export const BidPackageBuilder = ({ results, tender: selectedTender }) => {
const [manualDocuments, setManualDocuments] = useState(storedDraft.manualDocuments || []); const [manualDocuments, setManualDocuments] = useState(storedDraft.manualDocuments || []);
const [checks, setChecks] = useState(storedDraft.checks || {}); const [checks, setChecks] = useState(storedDraft.checks || {});
const documents = [...new Map([...(effectiveResults.documentChecklist || []), ...manualDocuments].map(d => [documentKey(d),d])).values()]; const documents = [...new Map([...(effectiveResults.documentChecklist || []), ...manualDocuments].map(d => [documentKey(d),d])).values()];
const [partialBid,setPartialBid] = useState(storedDraft.partialBid || {enabled:false,source:'',selected:[],assignments:{}});
const kalemler = sourceRows(tender, effectiveResults); const kalemler = sourceRows(tender, effectiveResults);
const sourceSnapshot = bidSourceSnapshot(tender, {...effectiveResults, documentChecklist:documents}, kalemler); const lotSelection = selectBidLots(kalemler,partialBid);
const allLots = [...new Set(kalemler.map(r=>rowLot(r,partialBid.assignments)).filter(Boolean))];
const sourceSnapshot = JSON.stringify([bidSourceSnapshot(tender, {...effectiveResults, documentChecklist:documents}, kalemler), partialBid]);
const [reviewedSource, setReviewedSource] = useState(storedDraft.reviewedSource || null); const [reviewedSource, setReviewedSource] = useState(storedDraft.reviewedSource || null);
const [fiyatlar, setFiyatlar] = useState(() => restoreBidPrices(storedDraft)); const [fiyatlar, setFiyatlar] = useState(() => restoreBidPrices(storedDraft));
const kaynakKontrolGerekli = reviewedSource !== sourceSnapshot; const kaynakKontrolGerekli = reviewedSource !== sourceSnapshot;
@@ -80,13 +84,13 @@ export const BidPackageBuilder = ({ results, tender: selectedTender }) => {
}; };
const satirlar = useMemo( const satirlar = useMemo(
() => kalemler.map(k => ({ ...k, birimFiyat: fiyatlar[k.priceKey] })), () => lotSelection.rows.map(k => ({ ...k, birimFiyat: fiyatlar[k.priceKey] })),
[kalemler, fiyatlar], [kalemler, fiyatlar, partialBid],
); );
const kalemToplami = useMemo(() => roundMoney(satirlar.reduce((t,r) => t + (bidRowTotal(r) ?? 0),0)), [satirlar]); const kalemToplami = useMemo(() => roundMoney(satirlar.reduce((t,r) => t + (bidRowTotal(r) ?? 0),0)), [satirlar]);
const saveDraft = async () => { const saveDraft = async () => {
const saved = await mutateTender(tender.id, {bidDraft:{firma,teklifTuru,gecerlilikGun,kisim,goturuBedel,priceSchema:1,pricesBySource:fiyatlar,sourceSnapshot,reviewedSource,checks,manualDocuments,analysisSource:{itemsAndSpecs:effectiveResults.itemsAndSpecs || [],documentChecklist:effectiveResults.documentChecklist || [],documentVersion:effectiveResults.documentVersion || null}}}); const saved = await mutateTender(tender.id, {bidDraft:{firma,teklifTuru,gecerlilikGun,kisim,goturuBedel,priceSchema:1,pricesBySource:fiyatlar,sourceSnapshot,reviewedSource,checks,manualDocuments,partialBid,analysisSource:{itemsAndSpecs:effectiveResults.itemsAndSpecs || [],documentChecklist:effectiveResults.documentChecklist || [],documentVersion:effectiveResults.documentVersion || null}}});
if (saved) { setDirty(false); setHata(null); } if (saved) { setDirty(false); setHata(null); }
else setHata('Taslak kaydedilemedi. Tekrar deneyin.'); else setHata('Taslak kaydedilemedi. Tekrar deneyin.');
}; };
@@ -97,14 +101,14 @@ export const BidPackageBuilder = ({ results, tender: selectedTender }) => {
const kalemEksik = teklifTuru === 'birim' && validateBidRows(satirlar).length > 0; const kalemEksik = teklifTuru === 'birim' && validateBidRows(satirlar).length > 0;
const turCeliskisi = teklifTuru === 'goturu' && /birim fiyat/i.test(tender?.aciklama || ''); const turCeliskisi = teklifTuru === 'goturu' && /birim fiyat/i.test(tender?.aciklama || '');
const sureEksik = !(Number(gecerlilikGun) > 0) || (kaynakGecerlilik && Number(gecerlilikGun) < Number(kaynakGecerlilik[1])); const sureEksik = !(Number(gecerlilikGun) > 0) || (kaynakGecerlilik && Number(gecerlilikGun) < Number(kaynakGecerlilik[1]));
const kontrolEksikleri = preparationBlockers(kalemler, documents, checks); const kontrolEksikleri = preparationBlockers(lotSelection.rows, documents, checks);
const hazir = eksikler.length === 0 && toplam > 0 && !kalemEksik && !turCeliskisi && !sureEksik && !kaynakKontrolGerekli && kontrolEksikleri.length === 0; const hazir = eksikler.length === 0 && toplam > 0 && !kalemEksik && !turCeliskisi && !sureEksik && !kaynakKontrolGerekli && kontrolEksikleri.length === 0 && lotSelection.errors.length === 0;
const formVerisi = () => ({ const formVerisi = () => ({
...firma, ...firma,
teklifTuru, teklifTuru,
gecerlilikGun, gecerlilikGun,
kisim, kisim: partialBid.enabled ? partialBid.selected.join(', ') : kisim,
idare: tender?.kurum || '', idare: tender?.kurum || '',
isinAdi: tender?.baslik || '', isinAdi: tender?.baslik || '',
ikn: tender?.ikn || '', ikn: tender?.ikn || '',
@@ -238,6 +242,27 @@ export const BidPackageBuilder = ({ results, tender: selectedTender }) => {
</label> </label>
</div> </div>
<section className="border rounded-xl p-3 space-y-2" aria-label="Kısmi teklif seçimi">
<label className="flex gap-2 text-xs font-bold"><input type="checkbox" checked={partialBid.enabled}
onChange={e=>setPartialBid(p=>({...p,enabled:e.target.checked}))}/>Yalnız seçtiğim resmi kısımlara teklif hazırlıyorum</label>
{partialBid.enabled && <>
<label className="block text-xs">Kısmi teklif izni — resmi doküman ve madde
<input className="w-full border rounded p-2" value={partialBid.source || ''} onChange={e=>setPartialBid(p=>({...p,source:e.target.value}))}/>
</label>
<p className="text-xs text-slate-600">Resmi kısım numaralarını cetvelden doğrulayın. Aynı kısımdaki tüm kalemler birlikte seçilir; seçilmeyenler toplam ve çıktı dışında kalır.</p>
<div className="flex flex-wrap gap-3">{allLots.map(lot=><label key={lot} className="text-xs flex gap-1">
<input type="checkbox" checked={(partialBid.selected || []).includes(lot)} onChange={e=>setPartialBid(p=>({...p,selected:e.target.checked ? [...(p.selected || []),lot] : (p.selected || []).filter(l=>l!==lot)}))}/>
Kısım {lot}
</label>)}</div>
<div className="max-h-64 overflow-auto space-y-2">{kalemler.map(row=><label key={row.priceKey} className="flex items-center gap-2 text-xs">
<span className="flex-1">{row.name}</span><span>Resmi kısım</span>
<input aria-label={`${row.name} resmi kısım`} className="border rounded p-1 w-24" value={rowLot(row,partialBid.assignments)} readOnly={row.kisim != null || row.lot != null}
onChange={e=>setPartialBid(p=>({...p,assignments:{...p.assignments,[row.priceKey]:e.target.value}}))}/>
</label>)}</div>
{lotSelection.errors.map(message=><p key={message} role="alert" className="text-xs text-red-700">{message}</p>)}
</>}
</section>
{/* Fiyatlandırma */} {/* Fiyatlandırma */}
{teklifTuru === 'goturu' ? ( {teklifTuru === 'goturu' ? (
<label className="block"> <label className="block">
@@ -251,7 +276,7 @@ export const BidPackageBuilder = ({ results, tender: selectedTender }) => {
className="w-full sm:w-64 text-sm font-bold px-3 py-2 rounded-lg border border-slate-300" className="w-full sm:w-64 text-sm font-bold px-3 py-2 rounded-lg border border-slate-300"
/> />
</label> </label>
) : kalemler.length > 0 ? ( ) : lotSelection.rows.length > 0 ? (
<div className="space-y-2"> <div className="space-y-2">
<h5 className="font-extrabold text-xs text-slate-900">Kalem fiyatlandırma</h5> <h5 className="font-extrabold text-xs text-slate-900">Kalem fiyatlandırma</h5>
<p className="text-xs text-slate-500">Ondalık fiyatı 125,50 veya 125.50 biçiminde yazın; binlik ayırıcı kullanmayın.</p> <p className="text-xs text-slate-500">Ondalık fiyatı 125,50 veya 125.50 biçiminde yazın; binlik ayırıcı kullanmayın.</p>
@@ -302,11 +327,11 @@ export const BidPackageBuilder = ({ results, tender: selectedTender }) => {
</div> </div>
) : ( ) : (
<p className="text-xs text-slate-500 bg-slate-50 border border-slate-200 rounded-lg p-3"> <p className="text-xs text-slate-500 bg-slate-50 border border-slate-200 rounded-lg p-3">
Kalem dökümü bulunamadı. Resmi teklif cetvelini İhale Dosyaları bölümünden yükleyin. Kalem eksikliği teklif türünü değiştirmez. {kalemler.length ? 'Henüz bir kısım seçilmedi. Yukarıdaki resmi kısım eşleştirmesini ve seçimini tamamlayın.' : 'Kalem dökümü bulunamadı. Resmi teklif cetvelini İhale Dosyaları bölümünden yükleyin. Kalem eksikliği teklif türünü değiştirmez.'}
</p> </p>
)} )}
<PreparationChecks rows={kalemler} documents={documents} manualDocuments={manualDocuments} <PreparationChecks rows={lotSelection.rows} documents={documents} manualDocuments={manualDocuments}
setManualDocuments={value => {setManualDocuments(value);setDirty(true);}} checks={checks} setChecks={setChecks}/> setManualDocuments={value => {setManualDocuments(value);setDirty(true);}} checks={checks} setChecks={setChecks}/>
{kontrolEksikleri.length > 0 && <details className="bg-amber-50 border border-amber-300 rounded-xl p-3"> {kontrolEksikleri.length > 0 && <details className="bg-amber-50 border border-amber-300 rounded-xl p-3">
<summary className="text-xs font-bold cursor-pointer">{kontrolEksikleri.length} belge/uygunluk kontrolü tamamlanmalı</summary> <summary className="text-xs font-bold cursor-pointer">{kontrolEksikleri.length} belge/uygunluk kontrolü tamamlanmalı</summary>
@@ -342,7 +367,7 @@ export const BidPackageBuilder = ({ results, tender: selectedTender }) => {
<div className="grid grid-cols-1 sm:grid-cols-3 gap-2.5"> <div className="grid grid-cols-1 sm:grid-cols-3 gap-2.5">
{[ {[
{ t: 'mektup', ad: 'Teklif Mektubu', ikon: FileSignature, aciklama: 'Rakam ve yazı ile bedel' }, { t: 'mektup', ad: 'Teklif Mektubu', ikon: FileSignature, aciklama: 'Rakam ve yazı ile bedel' },
{ t: 'cetvel', ad: 'Birim Fiyat Teklif Cetveli', ikon: Table2, aciklama: `${kalemler.length} kalem` }, { t: 'cetvel', ad: 'Birim Fiyat Teklif Cetveli', ikon: Table2, aciklama: `${lotSelection.rows.length} kalem` },
{ t: 'liste', ad: 'Evrak Kontrol Listesi', ikon: ListChecks, aciklama: `${documents.length} belge` }, { t: 'liste', ad: 'Evrak Kontrol Listesi', ikon: ListChecks, aciklama: `${documents.length} belge` },
].map((b) => ( ].map((b) => (
<button <button
+12
View File
@@ -0,0 +1,12 @@
export const rowLot = (row, assignments = {}) => String(row.kisim ?? row.lot ?? assignments[row.priceKey] ?? '').trim();
export function selectBidLots(rows, partial = {}) {
if (!partial.enabled) return {rows, errors:[],lots:[]};
const lots = [...new Set(rows.map(r=>rowLot(r,partial.assignments)).filter(Boolean))];
const selected = new Set(partial.selected || []);
const errors = [];
if (!partial.source?.trim()) errors.push('Kısmi teklif izninin resmi doküman ve madde kaynağını yazın.');
if (rows.some(r=>!rowLot(r,partial.assignments))) errors.push('Kısmi teklif için her kalemin resmi kısım numarası belirlenmeli.');
if (!lots.some(l=>selected.has(l))) errors.push('En az bir resmi kısım seçin.');
if ([...selected].some(l=>!lots.includes(l))) errors.push('Seçili bir kısım güncel kalemlerde bulunamadı; seçimi yeniden kontrol edin.');
return {rows:rows.filter(r=>selected.has(rowLot(r,partial.assignments))),errors,lots};
}
+1 -1
View File
@@ -5,7 +5,7 @@ export function sourceRows(tender, results) {
: (Array.isArray(results?.itemsAndSpecs) ? results.itemsAndSpecs : []); : (Array.isArray(results?.itemsAndSpecs) ? results.itemsAndSpecs : []);
const occurrences = new Map(); const occurrences = new Map();
return items.map(k => { return items.map(k => {
const identity = JSON.stringify([k.id ?? k.no ?? null, k.kisim ?? null, k.name, k.quantity, k.keySpecs || []]); const identity = JSON.stringify([k.id ?? k.no ?? null, k.kisim ?? k.lot ?? null, k.name, k.quantity, k.keySpecs || []]);
const occurrence = occurrences.get(identity) || 0; const occurrence = occurrences.get(identity) || 0;
occurrences.set(identity, occurrence + 1); occurrences.set(identity, occurrence + 1);
return { ...k, priceKey: JSON.stringify([identity, occurrence]) }; return { ...k, priceKey: JSON.stringify([identity, occurrence]) };
+21
View File
@@ -0,0 +1,21 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import {selectBidLots} from '../src/services/bidLots.js';
import {bidRowTotal} from '../src/services/bidValidation.js';
import {teklifCetveliHtml} from '../src/services/bidDocs.js';
const rows=[{priceKey:'a',name:'Kit A',kisim:'1',quantity:'2 adet',birimFiyat:10},{priceKey:'b',name:'Kit B',kisim:'1',quantity:'3 adet',birimFiyat:20},{priceKey:'c',name:'Cihaz C',kisim:'2',quantity:'1 adet',birimFiyat:1000}];
test('partial bid includes all selected-lot rows and excludes others from price and output',()=>{
const selected=selectBidLots(rows,{enabled:true,source:'İdari 19',selected:['1']});
assert.deepEqual(selected.errors,[]);assert.equal(selected.rows.length,2);
const total=selected.rows.reduce((sum,r)=>sum+bidRowTotal(r),0);assert.equal(total,80);
const html=teklifCetveliHtml({kisim:'1'},selected.rows,total);
assert.ok(html.includes('Kit A'));assert.ok(html.includes('Kit B'));assert.ok(!html.includes('Cihaz C'));
assert.equal(selectBidLots(rows,{enabled:false}).rows.length,3);
});
test('unknown grouping, source permission and obsolete selected lots prevent readiness',()=>{
assert.ok(selectBidLots(rows,{enabled:true,selected:['1']}).errors.length);
assert.ok(selectBidLots([{...rows[0],kisim:null}],{enabled:true,source:'19',selected:['1']}).errors.length);
assert.ok(selectBidLots(rows,{enabled:true,source:'19',selected:['3']}).errors.length);
const result=selectBidLots([{...rows[0],kisim:null}],{enabled:true,source:'19',selected:['1'],assignments:{a:'1'}});
assert.deepEqual(result.errors,[]);assert.equal(result.rows.length,1);
});